Step 2 - LDF Loading: - ldfparser integration (Python) / custom regex parser (C++) - QTreeWidget with expandable signal rows, merged Value column - Hex/Dec toggle, FreeFormat schedule entries, auto-reload - Baud rate auto-detection from LDF Step 3 - Signal Editing: - Bit packing/unpacking (signal value ↔ frame bytes) - ReadOnlyColumnDelegate for per-column editability - Value clamping to signal width, recursion guard Step 4 - Rx Panel: - receive_rx_frame() API with timestamp, signal unpacking - Change highlighting (yellow), auto-scroll toggle, clear button - Dashboard view (in-place update per frame_id) Step 5 - Connection Panel: - ConnectionManager with state machine (Disconnected/Connecting/Connected/Error) - Port scanning (pyserial / QSerialPort), connect/disconnect with UI mapping Step 6 - BabyLIN Backend: - BabyLinBackend wrapping Lipowsky BabyLIN_library.py DLL - Mock mode for macOS/CI, device scan, SDF loading, signal access - Frame callbacks, raw command access Step 7 - Master Scheduler: - QTimer-based schedule execution with start/stop/pause - Frame sent callback with visual highlighting - Mock Rx simulation, manual send, global rate override Tests: Python 171 | C++ 124 (Steps 1-5 parity, Steps 6-7 Python-first) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1159 lines
44 KiB
C++
1159 lines
44 KiB
C++
/**
|
|
* main_window.cpp — Implementation of the LIN Simulator main window.
|
|
*
|
|
* C++ equivalent of python/src/main_window.py.
|
|
* Uses QTreeWidget for expandable signal rows, merged Value column,
|
|
* Hex/Dec toggle, and ReadOnlyColumnDelegate.
|
|
*/
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// C++ CRASH COURSE FOR PYTHON DEVELOPERS
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
//
|
|
// If you know Python but not C++, here's what you need to read this file:
|
|
//
|
|
// 1. POINTERS AND ARROWS
|
|
// Python: widget.setText("hello") -- everything is a reference
|
|
// C++: widget->setText("hello") -- "->" accesses members via pointer
|
|
// value.toString() -- "." accesses members on a value
|
|
// Rule of thumb: variables created with "new" are pointers, use "->".
|
|
//
|
|
// 2. "auto*" — AUTOMATIC TYPE DEDUCTION
|
|
// auto* widget = new QPushButton("OK");
|
|
// The compiler figures out the type automatically. It's like Python where
|
|
// you never write types, except here "auto" makes it explicit that you're
|
|
// letting the compiler decide. The "*" means "this is a pointer".
|
|
//
|
|
// 3. "const auto&" — READ-ONLY REFERENCE (NO COPY)
|
|
// for (const auto& frame : data.tx_frames)
|
|
// This loops over the list WITHOUT copying each element. Like Python's
|
|
// "for frame in data.tx_frames" — Python never copies, but C++ does by
|
|
// default, so we use "&" (reference) to avoid it, and "const" to promise
|
|
// we won't modify it.
|
|
//
|
|
// 4. MEMORY: "new Widget(this)"
|
|
// C++ doesn't have garbage collection. But Qt has a parent-child system:
|
|
// "new QLabel(this)" creates a label owned by "this" window. When the
|
|
// window is destroyed, it automatically deletes all its children.
|
|
// So "new X(parent)" is safe — you don't need to manually delete it.
|
|
//
|
|
// 5. "::" — SCOPE RESOLUTION
|
|
// MainWindow::createMenuBar() — "createMenuBar belongs to MainWindow"
|
|
// Qt::Vertical — "Vertical is in the Qt namespace"
|
|
// QMainWindow::close — "close method from QMainWindow class"
|
|
// In Python this would be MainWindow.create_menu_bar, Qt.Vertical, etc.
|
|
//
|
|
// 6. "connect(sender, &Class::signal, receiver, &Class::slot)"
|
|
// Qt's signal-slot system — like Python's signal.connect(slot).
|
|
// The "&Class::signal" syntax is a pointer-to-member-function.
|
|
// Python: button.clicked.connect(self.on_load_ldf)
|
|
// C++: connect(button, &QPushButton::clicked, this, &MainWindow::onLoadLdf)
|
|
//
|
|
// 7. "const QString&" — PASS BY CONST REFERENCE
|
|
// Avoids copying the string. Python does this automatically (all objects
|
|
// are passed by reference). In C++ you must opt in with "&".
|
|
//
|
|
// 8. "override" — MARKS A METHOD THAT REPLACES A PARENT'S METHOD
|
|
// Like Python's method overriding, but the keyword lets the compiler
|
|
// verify the parent actually has that method (catches typos).
|
|
//
|
|
// 9. "{}" INITIALIZER LISTS
|
|
// QStringList list = {"a", "b", "c"}; — like Python's ["a", "b", "c"]
|
|
//
|
|
// 10. try/catch vs try/except
|
|
// C++: try { ... } catch (const std::exception& e) { e.what(); }
|
|
// Python: try: ... except Exception as e: str(e)
|
|
//
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
#include "main_window.h"
|
|
|
|
#include <QMenuBar>
|
|
#include <QMenu>
|
|
#include <QToolBar>
|
|
#include <QStatusBar>
|
|
#include <QDockWidget>
|
|
#include <QSplitter>
|
|
#include <QGroupBox>
|
|
#include <QTreeWidget>
|
|
#include <QTreeWidgetItem>
|
|
#include <QHeaderView>
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QPushButton>
|
|
#include <QComboBox>
|
|
#include <QCheckBox>
|
|
#include <QSpinBox>
|
|
#include <QFileDialog>
|
|
#include <QMessageBox>
|
|
#include <QAction>
|
|
#include <QFileSystemWatcher>
|
|
#include <QStyledItemDelegate>
|
|
#include <QRegularExpression>
|
|
#include <QDateTime>
|
|
|
|
// ─── ReadOnlyColumnDelegate ──────────────────────────────────────────
|
|
// C++ equivalent of Python's ReadOnlyColumnDelegate.
|
|
// Blocks editing on columns not in the editable set.
|
|
|
|
// This class inherits from QStyledItemDelegate (": public QStyledItemDelegate").
|
|
// It overrides one method to control which table columns are editable.
|
|
class ReadOnlyColumnDelegate : public QStyledItemDelegate
|
|
{
|
|
public:
|
|
// Constructor uses an INITIALIZER LIST (the part after the colon):
|
|
// : QStyledItemDelegate(parent), m_editableColumns(editableColumns)
|
|
// This is C++'s way of calling the parent constructor and initializing
|
|
// member variables. Python equivalent:
|
|
// super().__init__(parent)
|
|
// self.m_editable_columns = editable_columns
|
|
ReadOnlyColumnDelegate(QSet<int> editableColumns, QObject* parent = nullptr)
|
|
: QStyledItemDelegate(parent), m_editableColumns(editableColumns) {}
|
|
|
|
// "override" tells the compiler: "I'm replacing a method from the parent
|
|
// class." If the parent doesn't have this method, the compiler will error
|
|
// out — catching typos early. Python has no equivalent safety check.
|
|
//
|
|
// "const" at the end means this method doesn't modify the object.
|
|
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option,
|
|
const QModelIndex& index) const override
|
|
{
|
|
if (m_editableColumns.contains(index.column()))
|
|
// QStyledItemDelegate::createEditor — call the PARENT class's
|
|
// version. Like Python's super().createEditor(...)
|
|
return QStyledItemDelegate::createEditor(parent, option, index);
|
|
return nullptr; // Block editing (nullptr = Python's None)
|
|
}
|
|
|
|
private:
|
|
QSet<int> m_editableColumns; // QSet = Python's set()
|
|
};
|
|
|
|
// ─── Constructor ──────────────────────────────────────────────────────
|
|
|
|
// "MainWindow::MainWindow" — the "::" means "this constructor belongs to MainWindow".
|
|
// ": QMainWindow(parent)" is the initializer list — calls the parent constructor.
|
|
// Python equivalent: def __init__(self, parent=None): super().__init__(parent)
|
|
MainWindow::MainWindow(QWidget* parent)
|
|
: QMainWindow(parent)
|
|
{
|
|
setWindowTitle(tr("LIN Simulator"));
|
|
setMinimumSize(1024, 768);
|
|
|
|
// "new QFileSystemWatcher(this)" — allocates on the heap with "this" as
|
|
// the parent. Qt will auto-delete it when MainWindow is destroyed.
|
|
m_fileWatcher = new QFileSystemWatcher(this);
|
|
|
|
// Qt signal-slot connection — type-safe version:
|
|
// connect(WHO emits, WHICH signal, WHO receives, WHICH slot)
|
|
// Python equivalent:
|
|
// self.file_watcher.fileChanged.connect(self.on_ldf_file_changed)
|
|
// The "&Class::method" syntax is a pointer-to-member-function — it's how
|
|
// C++ references a specific method without calling it.
|
|
connect(m_fileWatcher, &QFileSystemWatcher::fileChanged,
|
|
this, &MainWindow::onLdfFileChanged);
|
|
|
|
createMenuBar();
|
|
createLdfToolbar();
|
|
createCentralWidget();
|
|
createConnectionDock();
|
|
createControlBar();
|
|
createStatusBar();
|
|
}
|
|
|
|
// ─── Menu Bar ─────────────────────────────────────────────────────────
|
|
|
|
void MainWindow::createMenuBar()
|
|
{
|
|
// "auto*" — the compiler deduces the type (QMenu*) from the return type
|
|
// of addMenu(). We write "auto*" instead of "QMenu*" for brevity.
|
|
// The "*" reminds us it's a pointer. Equivalent to Python:
|
|
// file_menu = self.menuBar().addMenu("&File")
|
|
auto* fileMenu = menuBar()->addMenu(tr("&File"));
|
|
|
|
// "new QAction(..., this)" — heap-allocated, owned by "this" MainWindow.
|
|
m_actionLoadLdf = new QAction(tr("&Load LDF..."), this);
|
|
m_actionLoadLdf->setShortcut(QKeySequence(tr("Ctrl+O")));
|
|
m_actionLoadLdf->setStatusTip(tr("Load a LIN Description File"));
|
|
connect(m_actionLoadLdf, &QAction::triggered, this, &MainWindow::onLoadLdf);
|
|
fileMenu->addAction(m_actionLoadLdf);
|
|
|
|
fileMenu->addSeparator();
|
|
|
|
auto* actionExit = new QAction(tr("E&xit"), this);
|
|
actionExit->setShortcut(QKeySequence(tr("Ctrl+Q")));
|
|
// Note: &QMainWindow::close — this connects to the PARENT class's close()
|
|
// method, not our own. "QMainWindow::" specifies which class the method
|
|
// belongs to (scope resolution).
|
|
connect(actionExit, &QAction::triggered, this, &QMainWindow::close);
|
|
fileMenu->addAction(actionExit);
|
|
|
|
m_viewMenu = menuBar()->addMenu(tr("&View"));
|
|
|
|
auto* helpMenu = menuBar()->addMenu(tr("&Help"));
|
|
auto* actionAbout = new QAction(tr("&About"), this);
|
|
connect(actionAbout, &QAction::triggered, this, &MainWindow::onAbout);
|
|
helpMenu->addAction(actionAbout);
|
|
}
|
|
|
|
// ─── LDF Toolbar ──────────────────────────────────────────────────────
|
|
|
|
void MainWindow::createLdfToolbar()
|
|
{
|
|
auto* toolbar = addToolBar(tr("LDF File"));
|
|
toolbar->setMovable(false);
|
|
|
|
toolbar->addWidget(new QLabel(tr(" LDF File: ")));
|
|
|
|
m_ldfPathEdit = new QLineEdit();
|
|
m_ldfPathEdit->setReadOnly(true);
|
|
m_ldfPathEdit->setPlaceholderText(tr("No LDF file loaded"));
|
|
m_ldfPathEdit->setMinimumWidth(300);
|
|
toolbar->addWidget(m_ldfPathEdit);
|
|
|
|
m_btnBrowse = new QPushButton(tr("Browse..."));
|
|
connect(m_btnBrowse, &QPushButton::clicked, this, &MainWindow::onLoadLdf);
|
|
toolbar->addWidget(m_btnBrowse);
|
|
|
|
m_chkAutoReload = new QCheckBox(tr("Auto-reload"));
|
|
m_chkAutoReload->setChecked(true);
|
|
m_chkAutoReload->setToolTip(
|
|
tr("Automatically reload the LDF file when it changes on disk")
|
|
);
|
|
toolbar->addWidget(m_chkAutoReload);
|
|
|
|
toolbar->addSeparator();
|
|
|
|
// Hex/Dec toggle — switches all Value columns between hex and decimal
|
|
m_chkHexMode = new QCheckBox(tr("Hex"));
|
|
m_chkHexMode->setChecked(true);
|
|
m_chkHexMode->setToolTip(tr("Toggle between hexadecimal and decimal display"));
|
|
connect(m_chkHexMode, &QCheckBox::toggled, this, &MainWindow::onHexModeToggled);
|
|
toolbar->addWidget(m_chkHexMode);
|
|
}
|
|
|
|
// ─── Central Widget (Tx + Rx Trees) ──────────────────────────────────
|
|
|
|
void MainWindow::createCentralWidget()
|
|
{
|
|
auto* central = new QWidget(this); // "this" = MainWindow is the parent
|
|
auto* layout = new QVBoxLayout(central); // passing "central" makes it the layout's parent
|
|
layout->setContentsMargins(4, 4, 4, 4);
|
|
|
|
// "Qt::Vertical" — the "::" accesses a value from the Qt namespace.
|
|
// Like Python's Qt.Vertical.
|
|
auto* splitter = new QSplitter(Qt::Vertical);
|
|
|
|
auto* txGroup = new QGroupBox(tr("Tx Frames (Master → Slave)"));
|
|
auto* txLayout = new QVBoxLayout(txGroup);
|
|
m_txTable = createTxTree();
|
|
connect(m_txTable, &QTreeWidget::itemChanged, this, &MainWindow::onTxItemChanged);
|
|
txLayout->addWidget(m_txTable);
|
|
splitter->addWidget(txGroup);
|
|
|
|
auto* rxGroup = new QGroupBox(tr("Rx Frames (Slave → Master)"));
|
|
auto* rxLayout = new QVBoxLayout(rxGroup);
|
|
|
|
auto* rxCtrlRow = new QHBoxLayout();
|
|
m_chkAutoScroll = new QCheckBox(tr("Auto-scroll"));
|
|
m_chkAutoScroll->setChecked(true);
|
|
rxCtrlRow->addWidget(m_chkAutoScroll);
|
|
|
|
m_btnClearRx = new QPushButton(tr("Clear"));
|
|
connect(m_btnClearRx, &QPushButton::clicked, this, &MainWindow::onClearRx);
|
|
rxCtrlRow->addWidget(m_btnClearRx);
|
|
rxCtrlRow->addStretch();
|
|
rxLayout->addLayout(rxCtrlRow);
|
|
|
|
m_rxTable = createRxTree();
|
|
rxLayout->addWidget(m_rxTable);
|
|
splitter->addWidget(rxGroup);
|
|
|
|
// {400, 400} is a C++ initializer list — like Python's [400, 400].
|
|
splitter->setSizes({400, 400});
|
|
layout->addWidget(splitter);
|
|
setCentralWidget(central);
|
|
}
|
|
|
|
QTreeWidget* MainWindow::createTxTree()
|
|
{
|
|
auto* tree = new QTreeWidget();
|
|
tree->setColumnCount(6);
|
|
tree->setHeaderLabels({
|
|
tr("Name"), tr("ID / Bit"), tr("Length / Width"),
|
|
tr("Interval (ms)"), tr("Value"), tr("Action")
|
|
});
|
|
|
|
auto* header = tree->header();
|
|
header->setSectionResizeMode(0, QHeaderView::Stretch); // Name
|
|
header->setSectionResizeMode(1, QHeaderView::ResizeToContents); // ID / Bit
|
|
header->setSectionResizeMode(2, QHeaderView::ResizeToContents); // Length / Width
|
|
header->setSectionResizeMode(3, QHeaderView::ResizeToContents); // Interval
|
|
header->setSectionResizeMode(4, QHeaderView::Stretch); // Value
|
|
header->setSectionResizeMode(5, QHeaderView::ResizeToContents); // Action
|
|
|
|
tree->setAlternatingRowColors(true);
|
|
tree->setRootIsDecorated(true);
|
|
|
|
// Only allow editing on Interval (col 3) and Value (col 4)
|
|
tree->setItemDelegate(new ReadOnlyColumnDelegate({3, 4}, tree));
|
|
|
|
return tree;
|
|
}
|
|
|
|
QTreeWidget* MainWindow::createRxTree()
|
|
{
|
|
auto* tree = new QTreeWidget();
|
|
tree->setColumnCount(5);
|
|
tree->setHeaderLabels({
|
|
tr("Timestamp"), tr("Name"), tr("ID / Bit"),
|
|
tr("Length / Width"), tr("Value")
|
|
});
|
|
|
|
auto* header = tree->header();
|
|
header->setSectionResizeMode(0, QHeaderView::ResizeToContents); // Timestamp
|
|
header->setSectionResizeMode(1, QHeaderView::Stretch); // Name
|
|
header->setSectionResizeMode(2, QHeaderView::ResizeToContents); // ID / Bit
|
|
header->setSectionResizeMode(3, QHeaderView::ResizeToContents); // Length / Width
|
|
header->setSectionResizeMode(4, QHeaderView::Stretch); // Value
|
|
|
|
tree->setAlternatingRowColors(true);
|
|
tree->setRootIsDecorated(true);
|
|
|
|
return tree;
|
|
}
|
|
|
|
// ─── Connection Dock Widget ───────────────────────────────────────────
|
|
|
|
void MainWindow::createConnectionDock()
|
|
{
|
|
auto* dock = new QDockWidget(tr("Connection"), this);
|
|
// "|" is bitwise OR — combines two flags into one value. Common pattern
|
|
// in C/C++ for combining options. Python's Qt bindings use the same syntax.
|
|
dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
|
|
|
|
auto* container = new QWidget();
|
|
auto* layout = new QVBoxLayout(container);
|
|
|
|
layout->addWidget(new QLabel(tr("Device:")));
|
|
|
|
auto* deviceRow = new QHBoxLayout();
|
|
m_comboDevice = new QComboBox();
|
|
m_comboDevice->setPlaceholderText(tr("Select device..."));
|
|
m_comboDevice->setMinimumWidth(150);
|
|
deviceRow->addWidget(m_comboDevice);
|
|
|
|
m_btnRefresh = new QPushButton(tr("Refresh"));
|
|
m_btnRefresh->setToolTip(tr("Scan for connected BabyLIN devices"));
|
|
connect(m_btnRefresh, &QPushButton::clicked, this, &MainWindow::onRefreshDevices);
|
|
deviceRow->addWidget(m_btnRefresh);
|
|
layout->addLayout(deviceRow);
|
|
|
|
layout->addWidget(new QLabel(tr("Baud Rate:")));
|
|
m_lblBaudRate = new QLabel(tr("— (load LDF)"));
|
|
m_lblBaudRate->setStyleSheet("font-weight: bold;");
|
|
m_lblBaudRate->setToolTip(
|
|
tr("LIN bus baud rate — automatically detected from the LDF file")
|
|
);
|
|
layout->addWidget(m_lblBaudRate);
|
|
|
|
auto* btnRow = new QHBoxLayout();
|
|
m_btnConnect = new QPushButton(tr("Connect"));
|
|
connect(m_btnConnect, &QPushButton::clicked, this, &MainWindow::onConnect);
|
|
m_btnDisconnect = new QPushButton(tr("Disconnect"));
|
|
connect(m_btnDisconnect, &QPushButton::clicked, this, &MainWindow::onDisconnect);
|
|
m_btnDisconnect->setEnabled(false);
|
|
btnRow->addWidget(m_btnConnect);
|
|
btnRow->addWidget(m_btnDisconnect);
|
|
layout->addLayout(btnRow);
|
|
|
|
m_lblConnStatus = new QLabel(tr("Status: Disconnected"));
|
|
m_lblConnStatus->setStyleSheet("color: red; font-weight: bold;");
|
|
layout->addWidget(m_lblConnStatus);
|
|
|
|
m_lblDeviceInfo = new QLabel(tr("Device Info: —"));
|
|
layout->addWidget(m_lblDeviceInfo);
|
|
|
|
layout->addStretch();
|
|
|
|
dock->setWidget(container);
|
|
addDockWidget(Qt::LeftDockWidgetArea, dock);
|
|
m_viewMenu->addAction(dock->toggleViewAction());
|
|
}
|
|
|
|
// ─── Control Bar ──────────────────────────────────────────────────────
|
|
|
|
void MainWindow::createControlBar()
|
|
{
|
|
auto* toolbar = new QToolBar(tr("Controls"));
|
|
toolbar->setMovable(false);
|
|
addToolBar(Qt::BottomToolBarArea, toolbar);
|
|
|
|
toolbar->addWidget(new QLabel(tr(" Schedule: ")));
|
|
m_comboSchedule = new QComboBox();
|
|
m_comboSchedule->setPlaceholderText(tr("No schedule loaded"));
|
|
m_comboSchedule->setMinimumWidth(200);
|
|
toolbar->addWidget(m_comboSchedule);
|
|
|
|
toolbar->addSeparator();
|
|
|
|
toolbar->addWidget(new QLabel(tr(" Global Rate (ms): ")));
|
|
m_spinGlobalRate = new QSpinBox();
|
|
m_spinGlobalRate->setRange(1, 10000);
|
|
m_spinGlobalRate->setValue(50);
|
|
m_spinGlobalRate->setSingleStep(10);
|
|
m_spinGlobalRate->setSuffix(tr(" ms"));
|
|
m_spinGlobalRate->setToolTip(
|
|
tr("Default send interval for all frames. "
|
|
"Per-frame intervals in the Tx table override this.")
|
|
);
|
|
toolbar->addWidget(m_spinGlobalRate);
|
|
|
|
toolbar->addSeparator();
|
|
|
|
m_btnStart = new QPushButton(tr("▶ Start"));
|
|
m_btnStop = new QPushButton(tr("■ Stop"));
|
|
m_btnPause = new QPushButton(tr("⏸ Pause"));
|
|
m_btnStart->setEnabled(false);
|
|
m_btnStop->setEnabled(false);
|
|
m_btnPause->setEnabled(false);
|
|
toolbar->addWidget(m_btnStart);
|
|
toolbar->addWidget(m_btnStop);
|
|
toolbar->addWidget(m_btnPause);
|
|
|
|
toolbar->addSeparator();
|
|
|
|
m_btnManualSend = new QPushButton(tr("Send Selected Frame"));
|
|
m_btnManualSend->setEnabled(false);
|
|
toolbar->addWidget(m_btnManualSend);
|
|
}
|
|
|
|
// ─── Status Bar ───────────────────────────────────────────────────────
|
|
|
|
void MainWindow::createStatusBar()
|
|
{
|
|
m_lblStatusConnection = new QLabel(tr("● Disconnected"));
|
|
m_lblStatusConnection->setStyleSheet("color: red;");
|
|
statusBar()->addPermanentWidget(m_lblStatusConnection);
|
|
statusBar()->showMessage(tr("Ready — Load an LDF file to begin"), 5000);
|
|
}
|
|
|
|
// ─── Slots ────────────────────────────────────────────────────────────
|
|
|
|
void MainWindow::onLoadLdf()
|
|
{
|
|
QString filePath = QFileDialog::getOpenFileName(
|
|
this, tr("Open LIN Description File"),
|
|
QString(), tr("LDF Files (*.ldf);;All Files (*)")
|
|
);
|
|
if (!filePath.isEmpty())
|
|
loadLdfFile(filePath);
|
|
}
|
|
|
|
// ─── LDF Loading ─────────────────────────────────────────────────────
|
|
|
|
// "const QString&" — pass by const reference. The "&" avoids copying the
|
|
// string (which could be expensive). "const" means we won't modify it.
|
|
// Python equivalent: def load_ldf_file(self, file_path: str)
|
|
void MainWindow::loadLdfFile(const QString& filePath)
|
|
{
|
|
// try/catch is C++'s version of Python's try/except.
|
|
// "const std::exception& e" catches any standard exception by reference.
|
|
try {
|
|
LdfData data = parseLdf(filePath);
|
|
// Assigning to std::optional — this sets it to "has a value" state.
|
|
// Like Python: self.ldf_data = data (vs self.ldf_data = None)
|
|
m_ldfData = data;
|
|
} catch (const std::exception& e) {
|
|
QMessageBox::critical(
|
|
this, tr("LDF Parse Error"),
|
|
tr("Failed to parse LDF file:\n\n%1\n\nError: %2")
|
|
.arg(filePath, QString::fromStdString(e.what()))
|
|
);
|
|
statusBar()->showMessage(
|
|
tr("Error loading LDF: %1").arg(QString::fromStdString(e.what())), 5000
|
|
);
|
|
return;
|
|
}
|
|
|
|
m_ldfPathEdit->setText(filePath);
|
|
// "m_ldfData->baudrate" — use "->" because m_ldfData is std::optional,
|
|
// and "->" accesses its contents. Like accessing an attribute in Python.
|
|
m_lblBaudRate->setText(tr("%1 baud").arg(m_ldfData->baudrate));
|
|
|
|
// "*m_ldfData" — the "*" dereferences the optional, giving us the
|
|
// actual LdfData value (not a pointer). We pass it by reference to
|
|
// the populate methods. In Python you'd just pass self.ldf_data directly.
|
|
populateTxTable(*m_ldfData);
|
|
populateRxTable(*m_ldfData);
|
|
populateScheduleCombo(*m_ldfData);
|
|
setupFileWatcher(filePath);
|
|
|
|
statusBar()->showMessage(
|
|
tr("LDF loaded: %1 | %2 Tx, %3 Rx frames | %4 baud")
|
|
.arg(m_ldfData->master_name)
|
|
.arg(m_ldfData->tx_frames.size())
|
|
.arg(m_ldfData->rx_frames.size())
|
|
.arg(m_ldfData->baudrate),
|
|
5000
|
|
);
|
|
}
|
|
|
|
// "const LdfData& data" — takes a reference to LdfData (no copy), read-only.
|
|
void MainWindow::populateTxTable(const LdfData& data)
|
|
{
|
|
m_txTable->clear();
|
|
m_txTable->setHeaderLabels({
|
|
tr("Name"), tr("ID / Bit"), tr("Length / Width"),
|
|
tr("Interval (ms)"), tr("Value"), tr("Action")
|
|
});
|
|
|
|
// Range-based for loop with "const auto&":
|
|
// "const" = we won't modify frame
|
|
// "auto" = compiler deduces the type (FrameInfo)
|
|
// "&" = reference, avoids copying each frame struct
|
|
// Python equivalent: for frame in data.tx_frames:
|
|
for (const auto& frame : data.tx_frames) {
|
|
// Frame bytes as zeros
|
|
QStringList hexBytes;
|
|
for (int i = 0; i < frame.length; ++i)
|
|
hexBytes << QStringLiteral("00");
|
|
|
|
// Frame row: Name | ID | Length | Interval | Value (bytes) | Action
|
|
auto* frameItem = new QTreeWidgetItem({
|
|
frame.name,
|
|
QStringLiteral("0x") + QString("%1").arg(frame.frame_id, 2, 16, QChar('0')).toUpper(),
|
|
QString::number(frame.length),
|
|
QString(), // Interval — filled by applyScheduleIntervals
|
|
QString(), // Value — filled by refreshValues
|
|
QString() // Action
|
|
});
|
|
|
|
// Store frame metadata for hex/dec conversion.
|
|
// Qt::UserRole — each tree item cell can store hidden data alongside
|
|
// displayed text. UserRole is a custom data slot (like a hidden
|
|
// attribute). This is how we store the raw numeric values while
|
|
// displaying formatted text.
|
|
frameItem->setData(0, Qt::UserRole, frame.frame_id);
|
|
// QVariantList — a list of QVariant values. QVariant is Qt's "any
|
|
// type" container (like Python's ability to put anything in a list).
|
|
// It can hold int, string, bool, etc.
|
|
QVariantList bytes;
|
|
for (int i = 0; i < frame.length; ++i)
|
|
bytes << 0;
|
|
frameItem->setData(4, Qt::UserRole, bytes);
|
|
|
|
// "flags() | Qt::ItemIsEditable" — bitwise OR adds the "editable" flag
|
|
// to whatever flags the item already has. Common C++ pattern for
|
|
// combining bit flags.
|
|
frameItem->setFlags(frameItem->flags() | Qt::ItemIsEditable);
|
|
|
|
// Signal child rows: Name | Bit pos | Width | — | Value | —
|
|
for (const auto& sig : frame.signal_list) {
|
|
auto* sigItem = new QTreeWidgetItem({
|
|
QStringLiteral(" ") + sig.name,
|
|
QStringLiteral("bit %1").arg(sig.bit_offset),
|
|
QStringLiteral("%1 bits").arg(sig.width),
|
|
QString(),
|
|
QString(), // Value — filled by refreshValues
|
|
QString()
|
|
});
|
|
sigItem->setData(4, Qt::UserRole, sig.init_value);
|
|
sigItem->setFlags(sigItem->flags() | Qt::ItemIsEditable);
|
|
frameItem->addChild(sigItem);
|
|
}
|
|
|
|
m_txTable->addTopLevelItem(frameItem);
|
|
}
|
|
|
|
refreshValues();
|
|
}
|
|
|
|
void MainWindow::populateRxTable(const LdfData& data)
|
|
{
|
|
m_rxTable->clear();
|
|
m_rxTable->setHeaderLabels({
|
|
tr("Timestamp"), tr("Name"), tr("ID / Bit"),
|
|
tr("Length / Width"), tr("Value")
|
|
});
|
|
|
|
for (const auto& frame : data.rx_frames) {
|
|
// Frame row: Timestamp | Name | ID | Length | Value
|
|
auto* frameItem = new QTreeWidgetItem({
|
|
QString::fromUtf8("—"),
|
|
frame.name,
|
|
QStringLiteral("0x") + QString("%1").arg(frame.frame_id, 2, 16, QChar('0')).toUpper(),
|
|
QString::number(frame.length),
|
|
QString::fromUtf8("—")
|
|
});
|
|
|
|
frameItem->setData(0, Qt::UserRole, frame.frame_id);
|
|
QVariantList bytes;
|
|
for (int i = 0; i < frame.length; ++i)
|
|
bytes << 0;
|
|
frameItem->setData(4, Qt::UserRole, bytes);
|
|
|
|
// Signal child rows
|
|
for (const auto& sig : frame.signal_list) {
|
|
auto* sigItem = new QTreeWidgetItem({
|
|
QString(),
|
|
QStringLiteral(" ") + sig.name,
|
|
QStringLiteral("bit %1").arg(sig.bit_offset),
|
|
QStringLiteral("%1 bits").arg(sig.width),
|
|
QString::fromUtf8("—")
|
|
});
|
|
sigItem->setData(4, Qt::UserRole, sig.init_value);
|
|
frameItem->addChild(sigItem);
|
|
}
|
|
|
|
m_rxTable->addTopLevelItem(frameItem);
|
|
}
|
|
}
|
|
|
|
void MainWindow::populateScheduleCombo(const LdfData& data)
|
|
{
|
|
m_comboSchedule->clear();
|
|
for (const auto& st : data.schedule_tables)
|
|
m_comboSchedule->addItem(st.name);
|
|
|
|
// "isEmpty()" is Qt's equivalent of Python's "not data.schedule_tables"
|
|
// or "len(data.schedule_tables) == 0".
|
|
if (!data.schedule_tables.isEmpty())
|
|
applyScheduleIntervals(data.schedule_tables[0]);
|
|
}
|
|
|
|
void MainWindow::applyScheduleIntervals(const ScheduleTableInfo& schedule)
|
|
{
|
|
if (!m_ldfData)
|
|
return;
|
|
|
|
// Build lookup, skip free-format entries
|
|
QMap<QString, int> delayMap;
|
|
for (const auto& entry : schedule.entries) {
|
|
if (entry.data.isEmpty()) // Regular frame entry, not FreeFormat
|
|
delayMap.insert(entry.frame_name, entry.delay_ms);
|
|
}
|
|
|
|
for (int i = 0; i < m_txTable->topLevelItemCount(); ++i) {
|
|
auto* item = m_txTable->topLevelItem(i);
|
|
QString frameName = item->text(0);
|
|
if (delayMap.contains(frameName))
|
|
item->setText(3, QString::number(delayMap[frameName]));
|
|
}
|
|
}
|
|
|
|
void MainWindow::setupFileWatcher(const QString& filePath)
|
|
{
|
|
QStringList watched = m_fileWatcher->files();
|
|
if (!watched.isEmpty())
|
|
m_fileWatcher->removePaths(watched);
|
|
m_fileWatcher->addPath(filePath);
|
|
}
|
|
|
|
// ─── Hex / Dec Display ───────────────────────────────────────────────
|
|
|
|
// "bool /*checked*/" — the parameter name is commented out because we don't
|
|
// use it. C++ requires you to declare parameter types even if unused.
|
|
// Commenting the name avoids "unused parameter" compiler warnings.
|
|
void MainWindow::onHexModeToggled(bool /*checked*/)
|
|
{
|
|
refreshValues();
|
|
}
|
|
|
|
void MainWindow::refreshValues()
|
|
{
|
|
m_updatingValues = true;
|
|
bool useHex = m_chkHexMode->isChecked();
|
|
|
|
// Refresh Tx tree
|
|
for (int i = 0; i < m_txTable->topLevelItemCount(); ++i) {
|
|
auto* frameItem = m_txTable->topLevelItem(i);
|
|
QVariantList bytes = frameItem->data(4, Qt::UserRole).toList();
|
|
if (!bytes.isEmpty()) {
|
|
QStringList parts;
|
|
for (const auto& b : bytes) {
|
|
if (useHex)
|
|
parts << QString("%1").arg(b.toInt(), 2, 16, QChar('0')).toUpper();
|
|
else
|
|
parts << QString::number(b.toInt());
|
|
}
|
|
frameItem->setText(4, parts.join(' '));
|
|
}
|
|
|
|
for (int j = 0; j < frameItem->childCount(); ++j) {
|
|
auto* sigItem = frameItem->child(j);
|
|
QVariant rawVal = sigItem->data(4, Qt::UserRole);
|
|
if (rawVal.isValid()) {
|
|
int val = rawVal.toInt();
|
|
if (useHex)
|
|
sigItem->setText(4, QStringLiteral("0x") + QString::number(val, 16).toUpper());
|
|
else
|
|
sigItem->setText(4, QString::number(val));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Refresh Rx tree
|
|
for (int i = 0; i < m_rxTable->topLevelItemCount(); ++i) {
|
|
auto* frameItem = m_rxTable->topLevelItem(i);
|
|
QVariantList bytes = frameItem->data(4, Qt::UserRole).toList();
|
|
bool hasData = false;
|
|
for (const auto& b : bytes) {
|
|
if (b.toInt() != 0) { hasData = true; break; }
|
|
}
|
|
if (hasData) {
|
|
QStringList parts;
|
|
for (const auto& b : bytes) {
|
|
if (useHex)
|
|
parts << QString("%1").arg(b.toInt(), 2, 16, QChar('0')).toUpper();
|
|
else
|
|
parts << QString::number(b.toInt());
|
|
}
|
|
frameItem->setText(4, parts.join(' '));
|
|
}
|
|
|
|
for (int j = 0; j < frameItem->childCount(); ++j) {
|
|
auto* sigItem = frameItem->child(j);
|
|
QVariant rawVal = sigItem->data(4, Qt::UserRole);
|
|
if (rawVal.isValid()) {
|
|
int val = rawVal.toInt();
|
|
if (useHex)
|
|
sigItem->setText(4, QStringLiteral("0x") + QString::number(val, 16).toUpper());
|
|
else
|
|
sigItem->setText(4, QString::number(val));
|
|
}
|
|
}
|
|
}
|
|
m_updatingValues = false;
|
|
}
|
|
|
|
// ─── Signal ↔ Frame Byte Sync (Step 3) ───────────────────────────────
|
|
|
|
void MainWindow::onTxItemChanged(QTreeWidgetItem* item, int column)
|
|
{
|
|
if (m_updatingValues || column != 4 || !m_ldfData)
|
|
return;
|
|
|
|
m_updatingValues = true;
|
|
|
|
// "auto*" deduces the type as QTreeWidgetItem*.
|
|
// "!parent" checks if the pointer is null (nullptr) — like Python's
|
|
// "if parent is None". A null pointer is falsy in C++.
|
|
auto* parent = item->parent();
|
|
if (!parent) {
|
|
// User edited a FRAME row's Value — unpack to signals
|
|
onFrameValueEdited(item);
|
|
} else {
|
|
// User edited a SIGNAL child's Value — pack into frame
|
|
onSignalValueEdited(item, parent);
|
|
}
|
|
|
|
m_updatingValues = false;
|
|
}
|
|
|
|
void MainWindow::onSignalValueEdited(QTreeWidgetItem* sigItem, QTreeWidgetItem* frameItem)
|
|
{
|
|
QString text = sigItem->text(4).trimmed();
|
|
bool ok;
|
|
int newVal;
|
|
if (text.toLower().startsWith("0x"))
|
|
newVal = text.mid(2).toInt(&ok, 16);
|
|
else
|
|
newVal = text.toInt(&ok);
|
|
|
|
if (!ok) {
|
|
refreshValues(); // Revert to stored value
|
|
return;
|
|
}
|
|
|
|
int sigIndex = frameItem->indexOfChild(sigItem);
|
|
int frameIndex = m_txTable->indexOfTopLevelItem(frameItem);
|
|
if (sigIndex < 0 || frameIndex < 0)
|
|
return;
|
|
|
|
// "const auto&" deduces the type as "const SignalInfo&" — a read-only
|
|
// reference to the signal info struct, no copy made.
|
|
const auto& sigInfo = m_ldfData->tx_frames[frameIndex].signal_list[sigIndex];
|
|
// "(1 << sigInfo.width) - 1" — bit shift to compute max value.
|
|
// e.g., width=8 gives (1<<8)-1 = 255. Like Python's (1 << width) - 1.
|
|
int maxVal = (1 << sigInfo.width) - 1;
|
|
// qBound(min, value, max) — Qt's clamp function. Like Python's
|
|
// max(0, min(new_val, max_val)).
|
|
newVal = qBound(0, newVal, maxVal);
|
|
|
|
sigItem->setData(4, Qt::UserRole, newVal);
|
|
repackFrameBytes(frameItem, frameIndex);
|
|
refreshValues();
|
|
}
|
|
|
|
void MainWindow::onFrameValueEdited(QTreeWidgetItem* frameItem)
|
|
{
|
|
QString text = frameItem->text(4).trimmed();
|
|
int frameIndex = m_txTable->indexOfTopLevelItem(frameItem);
|
|
if (frameIndex < 0 || !m_ldfData)
|
|
return;
|
|
|
|
const auto& frameInfo = m_ldfData->tx_frames[frameIndex];
|
|
|
|
// Parse bytes — support hex ("FF 80") or decimal ("255 128").
|
|
// "Qt::SkipEmptyParts" is an enum value — like Python's split() which
|
|
// automatically skips empty strings, but C++ split() keeps them by default.
|
|
QStringList parts = text.split(' ', Qt::SkipEmptyParts);
|
|
QVector<int> newBytes;
|
|
for (const auto& p : parts) {
|
|
bool ok;
|
|
int val;
|
|
// If 2 chars and all hex digits, treat as hex
|
|
if (p.length() <= 2 && p.contains(QRegularExpression("^[0-9a-fA-F]+$")))
|
|
val = p.toInt(&ok, 16);
|
|
else
|
|
val = p.toInt(&ok);
|
|
if (!ok) {
|
|
refreshValues();
|
|
return;
|
|
}
|
|
newBytes.append(val);
|
|
}
|
|
|
|
// Pad or truncate to frame length
|
|
while (newBytes.size() < frameInfo.length)
|
|
newBytes.append(0);
|
|
newBytes.resize(frameInfo.length);
|
|
|
|
// Store new bytes
|
|
QVariantList byteList;
|
|
for (int b : newBytes)
|
|
byteList << b;
|
|
// QVariantMap — Qt's dictionary with string keys and QVariant values.
|
|
// Like Python's dict[str, Any]. Square bracket access works just like Python.
|
|
QVariantMap frameData;
|
|
frameData["frame_id"] = frameInfo.frame_id;
|
|
frameItem->setData(0, Qt::UserRole, frameInfo.frame_id);
|
|
frameItem->setData(4, Qt::UserRole, byteList);
|
|
|
|
// Unpack signals from bytes
|
|
for (int i = 0; i < frameItem->childCount() && i < frameInfo.signal_list.size(); ++i) {
|
|
auto* sigItem = frameItem->child(i);
|
|
const auto& sigInfo = frameInfo.signal_list[i];
|
|
int value = extractSignal(newBytes, sigInfo.bit_offset, sigInfo.width);
|
|
sigItem->setData(4, Qt::UserRole, value);
|
|
}
|
|
|
|
refreshValues();
|
|
}
|
|
|
|
void MainWindow::repackFrameBytes(QTreeWidgetItem* frameItem, int frameIndex)
|
|
{
|
|
const auto& frameInfo = m_ldfData->tx_frames[frameIndex];
|
|
// "QVector<int> bytes(frameInfo.length, 0)" — creates a vector of
|
|
// frameInfo.length elements, all initialized to 0.
|
|
// Python equivalent: bytes = [0] * frame_info.length
|
|
QVector<int> bytes(frameInfo.length, 0);
|
|
|
|
for (int i = 0; i < frameItem->childCount() && i < frameInfo.signal_list.size(); ++i) {
|
|
auto* sigItem = frameItem->child(i);
|
|
const auto& sigInfo = frameInfo.signal_list[i];
|
|
int rawVal = sigItem->data(4, Qt::UserRole).toInt();
|
|
packSignal(bytes, sigInfo.bit_offset, sigInfo.width, rawVal);
|
|
}
|
|
|
|
// Store updated bytes
|
|
QVariantList byteList;
|
|
for (int b : bytes)
|
|
byteList << b;
|
|
frameItem->setData(4, Qt::UserRole, byteList);
|
|
}
|
|
|
|
// Static method — no "this" pointer, works on the passed-in data only.
|
|
// "QVector<int>& bytes" — non-const reference, so this function MODIFIES
|
|
// the caller's byte vector. Without "&" it would modify a local copy.
|
|
void MainWindow::packSignal(QVector<int>& bytes, int bitOffset, int width, int value)
|
|
{
|
|
for (int bit = 0; bit < width; ++bit) {
|
|
int byteIdx = (bitOffset + bit) / 8;
|
|
int bitIdx = (bitOffset + bit) % 8;
|
|
if (byteIdx < bytes.size()) {
|
|
// Bitwise operations — same syntax as Python:
|
|
// "&" = bitwise AND "|=" = bitwise OR-assign
|
|
// "~" = bitwise NOT "&=" = bitwise AND-assign
|
|
// "<<" = left shift
|
|
if (value & (1 << bit))
|
|
bytes[byteIdx] |= (1 << bitIdx); // Set bit to 1
|
|
else
|
|
bytes[byteIdx] &= ~(1 << bitIdx); // Clear bit to 0
|
|
}
|
|
}
|
|
}
|
|
|
|
int MainWindow::extractSignal(const QVector<int>& bytes, int bitOffset, int width)
|
|
{
|
|
int value = 0;
|
|
for (int bit = 0; bit < width; ++bit) {
|
|
int byteIdx = (bitOffset + bit) / 8;
|
|
int bitIdx = (bitOffset + bit) % 8;
|
|
if (byteIdx < bytes.size() && (bytes[byteIdx] & (1 << bitIdx)))
|
|
value |= (1 << bit);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// ─── Rx Panel: Real-time Data (Step 4) ────────────────────────────────
|
|
|
|
void MainWindow::receiveRxFrame(int frameId, const QVector<int>& dataBytes)
|
|
{
|
|
// "if (!m_ldfData)" — checks if the std::optional is empty (no value).
|
|
// Like Python's "if self.ldf_data is None: return"
|
|
if (!m_ldfData)
|
|
return;
|
|
|
|
for (int i = 0; i < m_rxTable->topLevelItemCount(); ++i) {
|
|
auto* frameItem = m_rxTable->topLevelItem(i);
|
|
int storedId = frameItem->data(0, Qt::UserRole).toInt();
|
|
if (storedId == frameId) {
|
|
updateRxFrame(frameItem, i, dataBytes);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::updateRxFrame(QTreeWidgetItem* frameItem, int frameIndex,
|
|
const QVector<int>& dataBytes)
|
|
{
|
|
// 1. Timestamp
|
|
frameItem->setText(0, QDateTime::currentDateTime().toString("HH:mm:ss.zzz"));
|
|
|
|
// 2. Store new bytes
|
|
QVariantList byteList;
|
|
for (int b : dataBytes)
|
|
byteList << b;
|
|
frameItem->setData(4, Qt::UserRole, byteList);
|
|
|
|
// 3. Unpack signals and detect changes
|
|
const auto& frameInfo = m_ldfData->rx_frames[frameIndex];
|
|
int fid = frameInfo.frame_id;
|
|
// ".value(fid)" — like Python's dict.get(fid, default). Returns a default-
|
|
// constructed QMap (empty) if the key doesn't exist. Safer than [] which
|
|
// would insert a default entry.
|
|
QMap<QString, int> prevValues = m_rxLastValues.value(fid);
|
|
QMap<QString, int> newValues;
|
|
|
|
for (int i = 0; i < frameItem->childCount() && i < frameInfo.signal_list.size(); ++i) {
|
|
auto* sigItem = frameItem->child(i);
|
|
const auto& sigInfo = frameInfo.signal_list[i];
|
|
int value = extractSignal(dataBytes, sigInfo.bit_offset, sigInfo.width);
|
|
sigItem->setData(4, Qt::UserRole, value);
|
|
newValues[sigInfo.name] = value;
|
|
|
|
// 4. Highlight if changed
|
|
if (prevValues.contains(sigInfo.name) && prevValues[sigInfo.name] != value)
|
|
sigItem->setBackground(4, QBrush(QColor(255, 255, 100)));
|
|
else
|
|
sigItem->setBackground(4, QBrush());
|
|
}
|
|
|
|
m_rxLastValues[fid] = newValues;
|
|
|
|
refreshRxFrame(frameItem);
|
|
|
|
// 5. Auto-scroll
|
|
if (m_chkAutoScroll->isChecked())
|
|
m_rxTable->scrollToItem(frameItem);
|
|
}
|
|
|
|
void MainWindow::refreshRxFrame(QTreeWidgetItem* frameItem)
|
|
{
|
|
bool useHex = m_chkHexMode->isChecked();
|
|
|
|
QVariantList bytes = frameItem->data(4, Qt::UserRole).toList();
|
|
if (!bytes.isEmpty()) {
|
|
QStringList parts;
|
|
for (const auto& b : bytes) {
|
|
if (useHex)
|
|
parts << QString("%1").arg(b.toInt(), 2, 16, QChar('0')).toUpper();
|
|
else
|
|
parts << QString::number(b.toInt());
|
|
}
|
|
frameItem->setText(4, parts.join(' '));
|
|
}
|
|
|
|
for (int j = 0; j < frameItem->childCount(); ++j) {
|
|
auto* sigItem = frameItem->child(j);
|
|
QVariant rawVal = sigItem->data(4, Qt::UserRole);
|
|
if (rawVal.isValid()) {
|
|
int val = rawVal.toInt();
|
|
if (useHex)
|
|
sigItem->setText(4, QStringLiteral("0x") + QString::number(val, 16).toUpper());
|
|
else
|
|
sigItem->setText(4, QString::number(val));
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::onClearRx()
|
|
{
|
|
if (!m_ldfData)
|
|
return;
|
|
|
|
m_rxLastValues.clear();
|
|
|
|
// Classic C-style for loop. "++i" increments i by 1 (same as i += 1).
|
|
// C++ range-based for can't be used here because we need the index i.
|
|
for (int i = 0; i < m_rxTable->topLevelItemCount(); ++i) {
|
|
auto* frameItem = m_rxTable->topLevelItem(i);
|
|
frameItem->setText(0, QString::fromUtf8("—"));
|
|
frameItem->setText(4, QString::fromUtf8("—"));
|
|
|
|
QVariantList zeros;
|
|
int frameLen = m_ldfData->rx_frames[i].length;
|
|
// "zeros << 0" — the "<<" operator is overloaded by Qt to mean
|
|
// "append". Like Python's zeros.append(0). In other contexts "<<"
|
|
// means left-shift or stream output — C++ reuses operators.
|
|
for (int j = 0; j < frameLen; ++j)
|
|
zeros << 0;
|
|
frameItem->setData(4, Qt::UserRole, zeros);
|
|
|
|
for (int j = 0; j < frameItem->childCount(); ++j) {
|
|
auto* sigItem = frameItem->child(j);
|
|
sigItem->setData(4, Qt::UserRole, 0);
|
|
sigItem->setText(4, QString::fromUtf8("—"));
|
|
sigItem->setBackground(4, QBrush());
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── File Watcher ─────────────────────────────────────────────────────
|
|
|
|
void MainWindow::onLdfFileChanged(const QString& path)
|
|
{
|
|
if (!m_chkAutoReload->isChecked())
|
|
return;
|
|
|
|
if (!m_fileWatcher->files().contains(path))
|
|
m_fileWatcher->addPath(path);
|
|
|
|
loadLdfFile(path);
|
|
statusBar()->showMessage(tr("LDF auto-reloaded: %1").arg(path), 3000);
|
|
}
|
|
|
|
// ─── Connection Panel (Step 5) ────────────────────────────────────────
|
|
|
|
void MainWindow::onRefreshDevices()
|
|
{
|
|
m_comboDevice->clear();
|
|
auto ports = m_connMgr.scanPorts();
|
|
|
|
if (ports.isEmpty()) {
|
|
m_comboDevice->setPlaceholderText(tr("No devices found"));
|
|
statusBar()->showMessage(tr("No serial ports found"), 3000);
|
|
return;
|
|
}
|
|
|
|
for (const auto& port : ports) {
|
|
QString display = QStringLiteral("%1 - %2").arg(port.device, port.description);
|
|
m_comboDevice->addItem(display, port.device);
|
|
}
|
|
statusBar()->showMessage(tr("Found %1 serial port(s)").arg(ports.size()), 3000);
|
|
}
|
|
|
|
void MainWindow::onConnect()
|
|
{
|
|
if (m_comboDevice->currentIndex() < 0) {
|
|
QMessageBox::warning(this, tr("No Device"), tr("Please select a device first."));
|
|
return;
|
|
}
|
|
|
|
QString portDevice = m_comboDevice->currentData().toString();
|
|
if (portDevice.isEmpty())
|
|
return;
|
|
|
|
int baudrate = 19200;
|
|
if (m_ldfData)
|
|
baudrate = m_ldfData->baudrate;
|
|
|
|
bool success = m_connMgr.connect(portDevice, baudrate);
|
|
updateConnectionUi();
|
|
|
|
if (success) {
|
|
statusBar()->showMessage(
|
|
tr("Connected to %1 at %2 baud").arg(portDevice).arg(baudrate), 3000);
|
|
} else {
|
|
QMessageBox::critical(this, tr("Connection Error"),
|
|
tr("Failed to connect to %1:\n\n%2").arg(portDevice, m_connMgr.errorMessage()));
|
|
}
|
|
}
|
|
|
|
void MainWindow::onDisconnect()
|
|
{
|
|
m_connMgr.disconnect();
|
|
updateConnectionUi();
|
|
statusBar()->showMessage(tr("Disconnected"), 3000);
|
|
}
|
|
|
|
void MainWindow::updateConnectionUi()
|
|
{
|
|
auto state = m_connMgr.state();
|
|
|
|
switch (state) {
|
|
case ConnectionState::Disconnected:
|
|
m_lblConnStatus->setText(tr("Status: Disconnected"));
|
|
m_lblConnStatus->setStyleSheet("color: red; font-weight: bold;");
|
|
m_btnConnect->setEnabled(true);
|
|
m_btnDisconnect->setEnabled(false);
|
|
m_lblDeviceInfo->setText(tr("Device Info: —"));
|
|
m_lblStatusConnection->setText(tr("● Disconnected"));
|
|
m_lblStatusConnection->setStyleSheet("color: red;");
|
|
break;
|
|
|
|
case ConnectionState::Connecting:
|
|
m_lblConnStatus->setText(tr("Status: Connecting..."));
|
|
m_lblConnStatus->setStyleSheet("color: orange; font-weight: bold;");
|
|
m_btnConnect->setEnabled(false);
|
|
m_btnDisconnect->setEnabled(false);
|
|
break;
|
|
|
|
case ConnectionState::Connected: {
|
|
m_lblConnStatus->setText(tr("Status: Connected"));
|
|
m_lblConnStatus->setStyleSheet("color: green; font-weight: bold;");
|
|
m_btnConnect->setEnabled(false);
|
|
m_btnDisconnect->setEnabled(true);
|
|
auto* port = m_connMgr.connectedPort();
|
|
if (port) {
|
|
m_lblDeviceInfo->setText(
|
|
tr("Device Info: %1\n%2").arg(port->device, port->description));
|
|
}
|
|
m_lblStatusConnection->setText(tr("● Connected"));
|
|
m_lblStatusConnection->setStyleSheet("color: green;");
|
|
break;
|
|
}
|
|
|
|
case ConnectionState::Error:
|
|
m_lblConnStatus->setText(
|
|
tr("Status: Error\n%1").arg(m_connMgr.errorMessage()));
|
|
m_lblConnStatus->setStyleSheet("color: red; font-weight: bold;");
|
|
m_btnConnect->setEnabled(true);
|
|
m_btnDisconnect->setEnabled(false);
|
|
m_lblStatusConnection->setText(tr("● Error"));
|
|
m_lblStatusConnection->setStyleSheet("color: red;");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void MainWindow::onAbout()
|
|
{
|
|
QMessageBox::about(
|
|
this,
|
|
tr("About LIN Simulator"),
|
|
tr("<h2>LIN Simulator</h2>"
|
|
"<p>Version 0.1.0</p>"
|
|
"<p>A cross-platform tool for simulating LIN master nodes "
|
|
"using BabyLIN devices.</p>"
|
|
"<hr>"
|
|
"<p><b>Owner:</b> TeqanyLogix LTD</p>"
|
|
"<p><b>Developer:</b> Mohamed Salem</p>"
|
|
"<hr>"
|
|
"<p>© 2026 TeqanyLogix LTD. All rights reserved.</p>")
|
|
);
|
|
}
|