/** * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ─── 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 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 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 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 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 bytes(frameInfo.length, 0)" — creates a vector of // frameInfo.length elements, all initialized to 0. // Python equivalent: bytes = [0] * frame_info.length QVector 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& bytes" — non-const reference, so this function MODIFIES // the caller's byte vector. Without "&" it would modify a local copy. void MainWindow::packSignal(QVector& 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& 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& 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& 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 prevValues = m_rxLastValues.value(fid); QMap 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("

LIN Simulator

" "

Version 0.1.0

" "

A cross-platform tool for simulating LIN master nodes " "using BabyLIN devices.

" "
" "

Owner: TeqanyLogix LTD

" "

Developer: Mohamed Salem

" "
" "

© 2026 TeqanyLogix LTD. All rights reserved.

") ); }