Lin_Simulator/cpp/src/main_window.cpp
Mohamed Salem cb60c2ad5d Steps 2-7: LDF loading, signal editing, Rx display, connection, BabyLIN backend, scheduler
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>
2026-04-04 14:21:24 +02:00

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>&copy; 2026 TeqanyLogix LTD. All rights reserved.</p>")
);
}