From b80877057382af2eec24c8f5a8f72bdcbd310950 Mon Sep 17 00:00:00 2001 From: Mohamed Salem Date: Thu, 2 Apr 2026 16:40:52 +0200 Subject: [PATCH] Step 1: GUI skeleton for LIN Simulator (Python + C++) - PyQt6 main window with Tx/Rx tables, connection dock, LDF toolbar, control bar with global send rate, and status bar - C++ Qt6 equivalent with identical layout and feature parity - About dialog: TeqanyLogix LTD / Developer: Mohamed Salem - Application logo (SVG + PNG) with LIN bus waveform design - Full test suites: Python (32 tests), C++ QTest (34 tests) - Project plan and Step 1 documentation Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 29 ++ PLAN.md | 207 ++++++++++++ cpp/CMakeLists.txt | 88 +++++ cpp/src/main.cpp | 51 +++ cpp/src/main_window.cpp | 372 +++++++++++++++++++++ cpp/src/main_window.h | 162 +++++++++ cpp/tests/test_main_window.cpp | 278 ++++++++++++++++ docs/step1_gui_skeleton.md | 89 +++++ python/requirements.txt | 2 + python/src/__init__.py | 0 python/src/main.py | 74 +++++ python/src/main_window.py | 547 +++++++++++++++++++++++++++++++ python/tests/__init__.py | 0 python/tests/test_main_window.py | 207 ++++++++++++ resources/logo.png | Bin 0 -> 77417 bytes resources/logo.svg | 75 +++++ 16 files changed, 2181 insertions(+) create mode 100644 .gitignore create mode 100644 PLAN.md create mode 100644 cpp/CMakeLists.txt create mode 100644 cpp/src/main.cpp create mode 100644 cpp/src/main_window.cpp create mode 100644 cpp/src/main_window.h create mode 100644 cpp/tests/test_main_window.cpp create mode 100644 docs/step1_gui_skeleton.md create mode 100644 python/requirements.txt create mode 100644 python/src/__init__.py create mode 100644 python/src/main.py create mode 100644 python/src/main_window.py create mode 100644 python/tests/__init__.py create mode 100644 python/tests/test_main_window.py create mode 100644 resources/logo.png create mode 100644 resources/logo.svg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9df17f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +venv/ +.venv/ + +# C++ +build/ +cmake-build-*/ +*.o +*.so +*.dylib +*.dll +*.exe + +# IDE +.vscode/ +.idea/ +*.swp + +# OS +.DS_Store +Thumbs.db + +# PyInstaller +*.spec diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..6d6577f --- /dev/null +++ b/PLAN.md @@ -0,0 +1,207 @@ +# LIN Master Simulator Tool — Implementation Plan + +## Context +Build a cross-platform LIN master simulator GUI using BabyLIN devices. Two parallel implementations: Python (PyQt6) and C++ (Qt6). Each step is built, verified, and tested before moving to the next. + +## Project Structure +``` +LIN_Control_Tool/ +├── python/ # Python implementation +│ ├── src/ +│ ├── tests/ +│ └── requirements.txt +├── cpp/ # C++ implementation +│ ├── src/ +│ ├── tests/ +│ └── CMakeLists.txt +└── resources/ # Shared: sample LDF files, icons, etc. +``` + +## Tech Stack +- **Python:** PyQt6, ldfparser, pyserial, pytest +- **C++:** Qt6 (Widgets, SerialPort), CMake, GoogleTest +- **LDF parsing (C++):** Custom or port of ldfparser logic + +## Step-by-Step Plan + +### Step 1 — GUI Skeleton +- **Status:** DONE — Python (32/32 tests) | C++ (34/34 tests) +- **Goal:** Main window with all panel placeholders laid out. + +**Panels:** +- Top bar: LDF file loader (path + browse button + auto-reload indicator) +- Left: Connection status panel (device dropdown, connect/disconnect, status LED) +- Center-top: Tx panel (table placeholder — frame name, ID, signals, data, send button) +- Center-bottom: Rx panel (table placeholder — timestamp, frame name, ID, data, signals) +- Bottom: Control bar (start/stop scheduler, manual send) + +**Python:** PyQt6 QMainWindow, QDockWidget or QSplitter layout +**C++:** Qt6 QMainWindow, same layout with .ui or code-based + +**Testing:** +- Launch app, verify all panels render correctly +- Resize window — panels resize proportionally +- Cross-platform: test on macOS, Windows, Linux + +--- + +### Step 2 — LDF Loading & Display +- **Status:** Not started +- **Goal:** Load an LDF file, parse it, populate Tx/Rx tables with frame/signal info. + +**Features:** +- File picker dialog +- Parse LDF using `ldfparser` (Python) / custom parser (C++) +- Populate Tx table with master frames (name, ID, length, signals) +- Populate Rx table columns (ready for runtime data) +- Auto-reload: watch file for changes, re-parse on modification +- Error handling for invalid LDF files + +**Python:** ldfparser library, QFileSystemWatcher for auto-reload +**C++:** Custom LDF parser, QFileSystemWatcher + +**Testing:** +- Load valid LDF → tables populate correctly +- Load invalid file → error message shown +- Modify LDF on disk → auto-reload triggers, tables update +- Verify parsed frame IDs, signal names, lengths match LDF content + +--- + +### Step 3 — Tx Panel (Signal Editing) +- **Status:** Not started +- **Goal:** Editable Tx table where user can modify signal values based on LDF types. + +**Features:** +- Each master frame row expandable to show individual signals +- Signal value editors based on type (bool → checkbox, int → spinbox, enum → dropdown) +- Frame data bytes update live as signals are edited +- Respect signal bit position, length, encoding from LDF +- Manual "Send" button per frame + +**Python:** QTreeWidget or QTableWidget with custom delegates +**C++:** Same with QStyledItemDelegate subclasses + +**Testing:** +- Edit a signal → frame data bytes reflect the change correctly +- Bool signal toggles between 0/1 +- Enum signal shows correct value names from LDF +- Integer signal respects min/max range from LDF +- Bit packing: verify multi-signal frames encode correctly + +--- + +### Step 4 — Rx Panel (Real-time Display) +- **Status:** Not started +- **Goal:** Rx table that shows incoming frames with timestamps. + +**Features:** +- Columns: timestamp, frame name, frame ID, raw data, decoded signals +- Expandable rows to see individual signal values +- Auto-scroll with pause option +- Clear button +- Signal highlighting on value change + +**Python:** QTableWidget with QTimer-based mock data for testing +**C++:** Same approach + +**Testing:** +- Feed mock Rx data → rows appear with correct timestamps +- Auto-scroll follows new data +- Pause stops scrolling, resume catches up +- Signal decode matches expected values +- Performance: handle 1000+ rows without lag + +--- + +### Step 5 — Connection Panel & Device Discovery +- **Status:** Not started +- **Goal:** Detect BabyLIN devices, show connection status. + +**Features:** +- Scan for available serial ports / BabyLIN devices +- Device dropdown with refresh button +- Connect / Disconnect buttons +- Status indicator (disconnected/connecting/connected/error) +- Display device info on connect (firmware version, etc.) + +**Python:** pyserial for port enumeration, QThread for connection +**C++:** QSerialPort, QSerialPortInfo + +**Testing:** +- No device → dropdown empty, status shows "No device" +- Mock serial port → connection succeeds, status turns green +- Disconnect → status updates, UI re-enables connect +- Unplug during session → error state shown, graceful handling + +--- + +### Step 6 — BabyLIN Communication Backend +- **Status:** Not started +- **Goal:** Implement the protocol layer for BabyLIN communication. + +**Features:** +- Serial protocol commands (init, send frame, receive frame) +- BabyLIN-DLL/SDK integration if available +- Abstract interface so serial vs SDK can be swapped +- Frame send/receive with proper LIN timing +- Error detection and reporting + +**Python:** pyserial + threading, abstract base class +**C++:** QSerialPort, abstract interface class + +**Testing:** +- Unit test protocol encoding/decoding +- Loopback test if hardware available +- Mock device for CI testing +- Verify frame timing within LIN spec tolerances + +--- + +### Step 7 — Master Scheduler +- **Status:** Not started +- **Goal:** Periodic frame transmission using LDF schedule tables. + +**Features:** +- Parse schedule tables from LDF +- Run selected schedule with correct timing (slot delays) +- Start/Stop/Pause controls +- Visual indication of currently transmitting frame +- Manual send overrides during schedule execution +- Schedule table selector dropdown + +**Python:** QTimer-based scheduler, QThread for timing accuracy +**C++:** QTimer, dedicated thread + +**Testing:** +- Start schedule → frames sent in correct order and timing +- Stop → transmission halts +- Manual send during schedule → frame injected correctly +- Verify slot timing accuracy (within tolerance) +- Switch schedule tables mid-run + +--- + +### Step 8 — Integration & End-to-End +- **Status:** Not started +- **Goal:** Wire all components together, full workflow testing. + +**Features:** +- Load LDF → connect device → start schedule → see Rx responses +- Edit Tx signals on-the-fly during active schedule +- Full error handling chain (device disconnect mid-run, etc.) +- Package as standalone executable (PyInstaller / CMake install) + +**Testing:** +- Full workflow with real BabyLIN + LIN slave device +- Stress test: extended run (hours), verify no memory leaks +- Cross-platform packaging and launch test +- Verify all UI states transition correctly + +--- + +## Verification Checklist (Every Step) +- [ ] Code review — clean, no warnings +- [ ] Unit tests pass (pytest / GoogleTest) +- [ ] Manual GUI test on at least one platform +- [ ] Both Python and C++ versions reach feature parity diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt new file mode 100644 index 0000000..20983c9 --- /dev/null +++ b/cpp/CMakeLists.txt @@ -0,0 +1,88 @@ +# CMakeLists.txt — Build configuration for the LIN Simulator (C++) +# +# CMAKE BASICS: +# ============= +# CMake is a build system generator. It doesn't compile code directly. +# Instead, it generates platform-specific build files: +# - macOS/Linux: Makefiles (then you run `make`) +# - Windows: Visual Studio project files +# - Any platform: Ninja build files (faster than Make) +# +# The workflow is: +# 1. mkdir build && cd build +# 2. cmake .. ← generates build files from this CMakeLists.txt +# 3. cmake --build . ← compiles and links the project +# 4. ./lin_simulator ← run the application +# +# Qt6 integration: +# CMake has built-in support for Qt via find_package(Qt6). +# Qt needs special preprocessing for its features: +# - MOC (Meta-Object Compiler): processes Q_OBJECT macros for signals/slots +# - UIC: compiles .ui designer files to C++ headers (we don't use these) +# - RCC: compiles resource files (icons, etc.) into the binary + +cmake_minimum_required(VERSION 3.16) + +project(LINSimulator + VERSION 0.1.0 + LANGUAGES CXX + DESCRIPTION "LIN Simulator using BabyLIN devices" +) + +# C++17 standard — required for structured bindings, std::optional, etc. +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# ── Qt6 auto-processing ── +# These three lines enable Qt's special preprocessors: +# AUTOMOC: automatically runs MOC on headers containing Q_OBJECT +# AUTOUIC: automatically compiles .ui files (if we had any) +# AUTORCC: automatically compiles .qrc resource files +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +# ── Find Qt6 ── +# find_package searches for Qt6 on the system. +# REQUIRED means CMake will error if Qt6 is not found. +# COMPONENTS lists which Qt modules we need: +# Widgets: GUI widgets (QMainWindow, QPushButton, etc.) +# We'll add SerialPort in Step 5 when we need device communication. +find_package(Qt6 REQUIRED COMPONENTS Widgets) + +# ── Main application target ── +# qt_add_executable is Qt's wrapper around add_executable. +# It handles platform-specific details (macOS app bundle, Windows subsystem, etc.) +qt_add_executable(lin_simulator + src/main.cpp + src/main_window.cpp + src/main_window.h +) + +# Link against Qt6 Widgets library +# PRIVATE means this dependency is only for building this target, +# not propagated to anything that depends on us. +target_link_libraries(lin_simulator PRIVATE Qt6::Widgets) + +# ── Tests ── +# We use Qt's built-in test framework (QTest) instead of GoogleTest +# because it integrates naturally with Qt's event loop and widgets. +# QTest provides: +# - QVERIFY(condition): assert a condition is true +# - QCOMPARE(actual, expected): assert two values are equal +# - QTest::mouseClick(): simulate mouse events +# - QTest::keyClick(): simulate keyboard events +enable_testing() +find_package(Qt6 REQUIRED COMPONENTS Test) + +qt_add_executable(test_main_window + tests/test_main_window.cpp + src/main_window.cpp + src/main_window.h +) + +target_link_libraries(test_main_window PRIVATE Qt6::Widgets Qt6::Test) +target_include_directories(test_main_window PRIVATE src) + +# Register the test with CTest so `ctest` can discover and run it +add_test(NAME test_main_window COMMAND test_main_window) diff --git a/cpp/src/main.cpp b/cpp/src/main.cpp new file mode 100644 index 0000000..45d1970 --- /dev/null +++ b/cpp/src/main.cpp @@ -0,0 +1,51 @@ +/** + * main.cpp — Application entry point for the LIN Simulator (C++). + * + * This is the C++ equivalent of python/src/main.py. + * + * C++ VS PYTHON STARTUP: + * ====================== + * Python: app = QApplication(sys.argv) → window = MainWindow() → app.exec() + * C++: QApplication app(argc, argv) → MainWindow window → app.exec() + * + * The key difference: in C++, objects can live on the STACK (local variable) + * or the HEAP (new/delete). Here, both `app` and `window` are stack objects. + * When main() returns, they're automatically destroyed — no garbage collector needed. + * + * Stack vs Heap: + * Stack: `MainWindow window;` — destroyed when function exits (automatic) + * Heap: `auto* w = new MainWindow;` — lives until explicitly deleted + * + * For the main window and app, stack allocation is perfect because they + * should live for the entire program duration and be cleaned up at exit. + */ + +#include +#include +#include +#include "main_window.h" + +int main(int argc, char* argv[]) +{ + // argc/argv are the C++ equivalent of sys.argv + // Qt uses them for platform-specific flags like --platform, --style, etc. + QApplication app(argc, argv); + + app.setApplicationName("LIN Simulator"); + app.setOrganizationName("TeqanyLogix LTD"); + + // Set application icon — resolves relative to the executable location. + // In the build directory, the icon is at ../../resources/logo.png + QString iconPath = QDir(QCoreApplication::applicationDirPath()) + .filePath("../../resources/logo.png"); + if (QFile::exists(iconPath)) { + app.setWindowIcon(QIcon(iconPath)); + } + + MainWindow window; + window.show(); + + // app.exec() enters the event loop — blocks until last window closes. + // Returns 0 on success, which main() returns to the OS. + return app.exec(); +} diff --git a/cpp/src/main_window.cpp b/cpp/src/main_window.cpp new file mode 100644 index 0000000..924778e --- /dev/null +++ b/cpp/src/main_window.cpp @@ -0,0 +1,372 @@ +/** + * main_window.cpp — Implementation of the LIN Simulator main window. + * + * This is the C++ equivalent of python/src/main_window.py. + * The structure mirrors the Python version exactly: + * createMenuBar() ↔ _create_menu_bar() + * createLdfToolbar() ↔ _create_ldf_toolbar() + * createCentralWidget() ↔ _create_central_widget() + * createConnectionDock()↔ _create_connection_dock() + * createControlBar() ↔ _create_control_bar() + * createStatusBar() ↔ _create_status_bar() + * + * KEY C++ / QT PATTERNS: + * ====================== + * 1. `new Widget(parent)` — parent takes ownership, no need to delete + * 2. `connect(sender, &Class::signal, receiver, &Class::slot)` — type-safe connections + * 3. `tr("text")` — marks strings for translation (internationalization) + * 4. `QString` — Qt's string class, supports Unicode, formatting, etc. + */ + +#include "main_window.h" + +// Now we include the full headers — needed because we're using these classes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ─── Constructor ────────────────────────────────────────────────────── + +MainWindow::MainWindow(QWidget* parent) + : QMainWindow(parent) // Initialize base class with parent + // ^^^ This is the "member initializer list" — it calls the base class + // constructor before the body runs. Required for QMainWindow. +{ + setWindowTitle(tr("LIN Simulator")); + // tr() wraps strings for Qt's translation system. Even if you never + // translate, it's a good habit — makes internationalization easy later. + + setMinimumSize(1024, 768); + + createMenuBar(); + createLdfToolbar(); + createCentralWidget(); + createConnectionDock(); + createControlBar(); + createStatusBar(); +} + +// ─── Menu Bar ───────────────────────────────────────────────────────── + +void MainWindow::createMenuBar() +{ + // menuBar() returns the QMainWindow's built-in menu bar (creates it if needed) + auto* fileMenu = menuBar()->addMenu(tr("&File")); + + // QAction in C++ — same concept as Python, but connection syntax differs: + // Python: action.triggered.connect(self._on_load_ldf) + // C++: connect(action, &QAction::triggered, this, &MainWindow::onLoadLdf) + // + // The C++ syntax uses pointers-to-member-functions which are type-checked + // at compile time — if onLoadLdf doesn't exist, compilation fails. + // Python only discovers the error at runtime. + 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"))); + connect(actionExit, &QAction::triggered, this, &QMainWindow::close); + fileMenu->addAction(actionExit); + + m_viewMenu = menuBar()->addMenu(tr("&View")); + + // Help menu + 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); +} + +// ─── Central Widget (Tx + Rx Panels) ───────────────────────────────── + +void MainWindow::createCentralWidget() +{ + // In C++, we must explicitly create a central widget and set it. + // `new QWidget(this)` — MainWindow becomes the parent/owner. + auto* central = new QWidget(this); + auto* layout = new QVBoxLayout(central); + layout->setContentsMargins(4, 4, 4, 4); + + // Qt::Vertical means the divider line is horizontal (widgets stack top/bottom) + auto* splitter = new QSplitter(Qt::Vertical); + + // Tx panel + auto* txGroup = new QGroupBox(tr("Tx Frames (Master → Slave)")); + auto* txLayout = new QVBoxLayout(txGroup); + m_txTable = createTxTable(); + txLayout->addWidget(m_txTable); + splitter->addWidget(txGroup); + + // Rx panel + auto* rxGroup = new QGroupBox(tr("Rx Frames (Slave → Master)")); + auto* rxLayout = new QVBoxLayout(rxGroup); + m_rxTable = createRxTable(); + rxLayout->addWidget(m_rxTable); + splitter->addWidget(rxGroup); + + splitter->setSizes({400, 400}); + + layout->addWidget(splitter); + setCentralWidget(central); +} + +QTableWidget* MainWindow::createTxTable() +{ + auto* table = new QTableWidget(); + table->setColumnCount(7); + table->setHorizontalHeaderLabels({ + tr("Frame Name"), tr("Frame ID"), tr("Length"), tr("Interval (ms)"), + tr("Data"), tr("Signals"), tr("Action") + }); + + // In C++, we access the enum values with the full scope path: + // QHeaderView::Stretch vs Python's QHeaderView.ResizeMode.Stretch + auto* header = table->horizontalHeader(); + header->setSectionResizeMode(0, QHeaderView::Stretch); // Frame Name + header->setSectionResizeMode(1, QHeaderView::ResizeToContents); // ID + header->setSectionResizeMode(2, QHeaderView::ResizeToContents); // Length + header->setSectionResizeMode(3, QHeaderView::ResizeToContents); // Interval + header->setSectionResizeMode(4, QHeaderView::Stretch); // Data + header->setSectionResizeMode(5, QHeaderView::Stretch); // Signals + header->setSectionResizeMode(6, QHeaderView::ResizeToContents); // Action + + table->setAlternatingRowColors(true); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + + return table; +} + +QTableWidget* MainWindow::createRxTable() +{ + auto* table = new QTableWidget(); + table->setColumnCount(5); + table->setHorizontalHeaderLabels({ + tr("Timestamp"), tr("Frame Name"), tr("Frame ID"), + tr("Data"), tr("Signals") + }); + + auto* header = table->horizontalHeader(); + header->setSectionResizeMode(0, QHeaderView::ResizeToContents); // Timestamp + header->setSectionResizeMode(1, QHeaderView::Stretch); // Frame Name + header->setSectionResizeMode(2, QHeaderView::ResizeToContents); // ID + header->setSectionResizeMode(3, QHeaderView::Stretch); // Data + header->setSectionResizeMode(4, QHeaderView::Stretch); // Signals + + table->setAlternatingRowColors(true); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + + return table; +} + +// ─── Connection Dock Widget ─────────────────────────────────────────── + +void MainWindow::createConnectionDock() +{ + auto* dock = new QDockWidget(tr("Connection"), this); + dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + + auto* container = new QWidget(); + auto* layout = new QVBoxLayout(container); + + // Device selection + 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")); + deviceRow->addWidget(m_btnRefresh); + layout->addLayout(deviceRow); + + // Baud rate display (read-only, from LDF) + 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); + + // Connect / Disconnect + auto* btnRow = new QHBoxLayout(); + m_btnConnect = new QPushButton(tr("Connect")); + m_btnDisconnect = new QPushButton(tr("Disconnect")); + m_btnDisconnect->setEnabled(false); + btnRow->addWidget(m_btnConnect); + btnRow->addWidget(m_btnDisconnect); + layout->addLayout(btnRow); + + // Status + m_lblConnStatus = new QLabel(tr("Status: Disconnected")); + m_lblConnStatus->setStyleSheet("color: red; font-weight: bold;"); + layout->addWidget(m_lblConnStatus); + + // Device info + m_lblDeviceInfo = new QLabel(tr("Device Info: —")); + layout->addWidget(m_lblDeviceInfo); + + layout->addStretch(); + + dock->setWidget(container); + addDockWidget(Qt::LeftDockWidgetArea, dock); + + // toggleViewAction() returns a QAction that shows/hides the dock + m_viewMenu->addAction(dock->toggleViewAction()); +} + +// ─── Control Bar (Bottom Toolbar) ───────────────────────────────────── + +void MainWindow::createControlBar() +{ + auto* toolbar = new QToolBar(tr("Controls")); + toolbar->setMovable(false); + addToolBar(Qt::BottomToolBarArea, toolbar); + + // Schedule selector + 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(); + + // Global send rate + 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(); + + // Scheduler controls + 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(); + + // Manual send + m_btnManualSend = new QPushButton(tr("Send Selected Frame")); + m_btnManualSend->setEnabled(false); + toolbar->addWidget(m_btnManualSend); +} + +// ─── Status Bar ─────────────────────────────────────────────────────── + +void MainWindow::createStatusBar() +{ + // statusBar() creates the status bar if it doesn't exist yet + 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() +{ + // QFileDialog::getOpenFileName returns a QString. + // In C++, empty string check is .isEmpty() instead of Python's truthiness. + QString filePath = QFileDialog::getOpenFileName( + this, + tr("Open LIN Description File"), + QString(), // Empty = last used directory + tr("LDF Files (*.ldf);;All Files (*)") + ); + + if (!filePath.isEmpty()) { + m_ldfPathEdit->setText(filePath); + statusBar()->showMessage( + tr("LDF file selected: %1").arg(filePath), 3000 + ); + // %1 is Qt's string formatting placeholder — .arg() substitutes it. + // Similar to Python's f-string: f"LDF file selected: {file_path}" + } +} + +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.

") + ); +} diff --git a/cpp/src/main_window.h b/cpp/src/main_window.h new file mode 100644 index 0000000..ae6d9c9 --- /dev/null +++ b/cpp/src/main_window.h @@ -0,0 +1,162 @@ +/** + * main_window.h — Header file for the LIN Simulator main window. + * + * HEADER FILES IN C++: + * ==================== + * Unlike Python where everything is in one file, C++ splits code into: + * - Header (.h): DECLARES what exists (class names, function signatures) + * - Source (.cpp): DEFINES how it works (function bodies, logic) + * + * Why? Because C++ compiles each .cpp file independently. Headers let + * different .cpp files know about each other's classes without seeing + * the full implementation. Think of it like a table of contents. + * + * Q_OBJECT MACRO: + * =============== + * Any class that uses Qt's signals/slots MUST include Q_OBJECT at the top. + * This tells Qt's Meta-Object Compiler (MOC) to generate extra code that + * enables runtime introspection — Qt needs this for: + * - Signal/slot connections + * - Property system + * - Dynamic casting with qobject_cast + * + * MOC reads this header, generates a moc_main_window.cpp file with the + * glue code, and the build system compiles it automatically (CMAKE_AUTOMOC). + */ + +#ifndef MAIN_WINDOW_H +#define MAIN_WINDOW_H +// ^^^ "Include guard" — prevents this header from being included twice +// in the same compilation unit, which would cause duplicate definition errors. + +#include + +// Forward declarations — tell the compiler these classes exist without +// including their full headers. This speeds up compilation because we +// only need the full definition in the .cpp file where we use them. +// Think of it as saying "trust me, QTableWidget exists, I'll show you later." +class QTableWidget; +class QLineEdit; +class QPushButton; +class QComboBox; +class QCheckBox; +class QSpinBox; +class QLabel; +class QAction; + +/** + * MainWindow — The root window of the LIN Simulator. + * + * Inherits from QMainWindow which provides: + * - Menu bar, toolbars, dock areas, status bar + * - Central widget area + * + * PARENT-CHILD OWNERSHIP: + * All widgets created with `new Widget(parent)` are owned by their parent. + * When MainWindow is destroyed, it automatically destroys all children. + * This is why we use raw pointers (Widget*) without delete — Qt manages it. + */ +class MainWindow : public QMainWindow +{ + Q_OBJECT // Required for signals/slots — MOC processes this + +public: + /** + * Constructor. + * @param parent Parent widget (nullptr for top-level window). + * + * In Qt, every widget can have a parent. For the main window, + * parent is nullptr because it's the top-level window with no owner. + */ + explicit MainWindow(QWidget* parent = nullptr); + + // No destructor needed — Qt's parent-child system handles cleanup. + // All the pointers below are children of MainWindow and will be + // automatically deleted when MainWindow is destroyed. + + // ── Public accessors for testing ── + // These let tests verify the widget tree without exposing internals. + // In Python we accessed attributes directly (window.tx_table). + // In C++ we use getter functions — this is idiomatic C++ encapsulation. + QTableWidget* txTable() const { return m_txTable; } + QTableWidget* rxTable() const { return m_rxTable; } + QLineEdit* ldfPathEdit() const { return m_ldfPathEdit; } + QPushButton* browseButton() const { return m_btnBrowse; } + QCheckBox* autoReloadCheck() const { return m_chkAutoReload; } + QComboBox* deviceCombo() const { return m_comboDevice; } + QPushButton* connectButton() const { return m_btnConnect; } + QPushButton* disconnectButton() const { return m_btnDisconnect; } + QLabel* connStatusLabel() const { return m_lblConnStatus; } + QLabel* baudRateLabel() const { return m_lblBaudRate; } + QLabel* deviceInfoLabel() const { return m_lblDeviceInfo; } + QComboBox* scheduleCombo() const { return m_comboSchedule; } + QSpinBox* globalRateSpin() const { return m_spinGlobalRate; } + QPushButton* startButton() const { return m_btnStart; } + QPushButton* stopButton() const { return m_btnStop; } + QPushButton* pauseButton() const { return m_btnPause; } + QPushButton* manualSendButton() const { return m_btnManualSend; } + QLabel* statusConnectionLabel() const { return m_lblStatusConnection; } + QAction* loadLdfAction() const { return m_actionLoadLdf; } + +private slots: + // ── Slots ── + // In C++, slots are declared in a special section. The `slots` keyword + // is a Qt macro that MOC processes. These are functions that can be + // connected to signals. + void onLoadLdf(); + void onAbout(); + +private: + // ── Setup methods ── + void createMenuBar(); + void createLdfToolbar(); + void createCentralWidget(); + void createConnectionDock(); + void createControlBar(); + void createStatusBar(); + + // ── Helper methods ── + QTableWidget* createTxTable(); + QTableWidget* createRxTable(); + + // ── Member variables ── + // Convention: m_ prefix for member variables (common in Qt/C++ codebases). + // All are raw pointers — Qt's parent-child system manages their lifetime. + + // LDF toolbar + QLineEdit* m_ldfPathEdit; + QPushButton* m_btnBrowse; + QCheckBox* m_chkAutoReload; + + // Central tables + QTableWidget* m_txTable; + QTableWidget* m_rxTable; + + // Connection dock + QComboBox* m_comboDevice; + QPushButton* m_btnRefresh; + QPushButton* m_btnConnect; + QPushButton* m_btnDisconnect; + QLabel* m_lblConnStatus; + QLabel* m_lblBaudRate; + QLabel* m_lblDeviceInfo; + + // Control bar + QComboBox* m_comboSchedule; + QSpinBox* m_spinGlobalRate; + QPushButton* m_btnStart; + QPushButton* m_btnStop; + QPushButton* m_btnPause; + QPushButton* m_btnManualSend; + + // Status bar + QLabel* m_lblStatusConnection; + + // Actions + QAction* m_actionLoadLdf; + + // View menu (need to store to add dock toggle actions) + QMenu* m_viewMenu; +}; + +#endif // MAIN_WINDOW_H diff --git a/cpp/tests/test_main_window.cpp b/cpp/tests/test_main_window.cpp new file mode 100644 index 0000000..b6f7eea --- /dev/null +++ b/cpp/tests/test_main_window.cpp @@ -0,0 +1,278 @@ +/** + * test_main_window.cpp — Tests for the GUI skeleton (Step 1, C++). + * + * QTest FRAMEWORK: + * ================ + * Qt provides its own test framework, QTest. It's simpler than GoogleTest + * but integrates perfectly with Qt's event loop and widget system. + * + * Key differences from pytest: + * pytest: assert window.windowTitle() == "..." + * QTest: QCOMPARE(window.windowTitle(), QString("...")) + * + * Test discovery: + * - Each test class inherits QObject and has Q_OBJECT + * - Test methods are private slots named test_*() or *_test() + * - QTEST_MAIN() generates main() that runs all test slots + * + * QCOMPARE vs QVERIFY: + * QVERIFY(condition) — like assert condition + * QCOMPARE(actual, expected) — like assert actual == expected, but prints both values on failure + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "main_window.h" + +class TestMainWindow : public QObject +{ + Q_OBJECT + +private: + MainWindow* m_window; + +private slots: + // ── Setup/Teardown ── + // init() runs BEFORE each test, cleanup() runs AFTER each test. + // This gives each test a fresh MainWindow (like pytest fixtures). + void init() + { + m_window = new MainWindow(); + } + + void cleanup() + { + delete m_window; + m_window = nullptr; + } + + // ─── Window Basics ──────────────────────────────────────────── + + void test_windowTitle() + { + QCOMPARE(m_window->windowTitle(), QString("LIN Simulator")); + } + + void test_minimumSize() + { + QVERIFY(m_window->minimumWidth() >= 1024); + QVERIFY(m_window->minimumHeight() >= 768); + } + + void test_centralWidgetExists() + { + QVERIFY(m_window->centralWidget() != nullptr); + } + + // ─── Menu Bar ───────────────────────────────────────────────── + + void test_menuBarExists() + { + QVERIFY(m_window->menuBar() != nullptr); + } + + void test_loadLdfActionExists() + { + QVERIFY(m_window->loadLdfAction() != nullptr); + QCOMPARE(m_window->loadLdfAction()->text(), QString("&Load LDF...")); + } + + void test_loadLdfShortcut() + { + QCOMPARE(m_window->loadLdfAction()->shortcut().toString(), QString("Ctrl+O")); + } + + // ─── LDF Toolbar ────────────────────────────────────────────── + + void test_ldfPathFieldExists() + { + QVERIFY(m_window->ldfPathEdit() != nullptr); + QVERIFY(m_window->ldfPathEdit()->isReadOnly()); + } + + void test_ldfPathPlaceholder() + { + QCOMPARE(m_window->ldfPathEdit()->placeholderText(), + QString("No LDF file loaded")); + } + + void test_browseButtonExists() + { + QVERIFY(m_window->browseButton() != nullptr); + } + + void test_autoReloadDefaultChecked() + { + QVERIFY(m_window->autoReloadCheck()->isChecked()); + } + + // ─── Tx Table ───────────────────────────────────────────────── + + void test_txTableExists() + { + auto* table = m_window->txTable(); + QVERIFY(table != nullptr); + // qobject_cast is Qt's type-safe dynamic cast. + // Returns nullptr if the object isn't the expected type. + QVERIFY(qobject_cast(table) != nullptr); + } + + void test_txTableColumns() + { + auto* table = m_window->txTable(); + QCOMPARE(table->columnCount(), 7); + + QStringList expected = { + "Frame Name", "Frame ID", "Length", "Interval (ms)", + "Data", "Signals", "Action" + }; + for (int i = 0; i < table->columnCount(); ++i) { + QCOMPARE(table->horizontalHeaderItem(i)->text(), expected[i]); + } + } + + void test_txTableAlternatingColors() + { + QVERIFY(m_window->txTable()->alternatingRowColors()); + } + + // ─── Rx Table ───────────────────────────────────────────────── + + void test_rxTableExists() + { + auto* table = m_window->rxTable(); + QVERIFY(table != nullptr); + QVERIFY(qobject_cast(table) != nullptr); + } + + void test_rxTableColumns() + { + auto* table = m_window->rxTable(); + QCOMPARE(table->columnCount(), 5); + + QStringList expected = { + "Timestamp", "Frame Name", "Frame ID", "Data", "Signals" + }; + for (int i = 0; i < table->columnCount(); ++i) { + QCOMPARE(table->horizontalHeaderItem(i)->text(), expected[i]); + } + } + + void test_rxTableNotEditable() + { + QCOMPARE(m_window->rxTable()->editTriggers(), + QAbstractItemView::NoEditTriggers); + } + + // ─── Connection Dock ────────────────────────────────────────── + + void test_dockExists() + { + // findChildren() searches the widget tree for all children of type T + auto docks = m_window->findChildren(); + QCOMPARE(docks.size(), 1); + QCOMPARE(docks[0]->windowTitle(), QString("Connection")); + } + + void test_deviceComboExists() + { + QVERIFY(m_window->deviceCombo() != nullptr); + } + + void test_connectButtonExists() + { + QVERIFY(m_window->connectButton() != nullptr); + QVERIFY(m_window->connectButton()->isEnabled()); + } + + void test_disconnectButtonDisabledInitially() + { + QVERIFY(!m_window->disconnectButton()->isEnabled()); + } + + void test_statusLabelShowsDisconnected() + { + QVERIFY(m_window->connStatusLabel()->text().contains("Disconnected")); + } + + void test_baudRateLabelExists() + { + QVERIFY(m_window->baudRateLabel() != nullptr); + } + + void test_baudRateShowsPlaceholderBeforeLdf() + { + QVERIFY(m_window->baudRateLabel()->text().contains("load LDF")); + } + + // ─── Control Bar ────────────────────────────────────────────── + + void test_scheduleComboExists() + { + QVERIFY(m_window->scheduleCombo() != nullptr); + } + + void test_schedulerButtonsDisabledInitially() + { + QVERIFY(!m_window->startButton()->isEnabled()); + QVERIFY(!m_window->stopButton()->isEnabled()); + QVERIFY(!m_window->pauseButton()->isEnabled()); + } + + void test_manualSendDisabledInitially() + { + QVERIFY(!m_window->manualSendButton()->isEnabled()); + } + + void test_globalRateSpinboxExists() + { + QVERIFY(m_window->globalRateSpin() != nullptr); + } + + void test_globalRateDefault50ms() + { + QCOMPARE(m_window->globalRateSpin()->value(), 50); + } + + void test_globalRateRange() + { + QCOMPARE(m_window->globalRateSpin()->minimum(), 1); + QCOMPARE(m_window->globalRateSpin()->maximum(), 10000); + } + + void test_globalRateSuffix() + { + QCOMPARE(m_window->globalRateSpin()->suffix(), QString(" ms")); + } + + // ─── Status Bar ─────────────────────────────────────────────── + + void test_statusBarExists() + { + QVERIFY(m_window->statusBar() != nullptr); + } + + void test_connectionStatusLabel() + { + QVERIFY(m_window->statusConnectionLabel()->text().contains("Disconnected")); + } +}; + +// QTEST_MAIN generates a main() function that: +// 1. Creates a QApplication +// 2. Instantiates TestMainWindow +// 3. Runs all private slots as test cases +// 4. Reports results +QTEST_MAIN(TestMainWindow) + +// This #include is required when the test class is defined in a .cpp file +// (not a .h file). It includes the MOC-generated code for our Q_OBJECT class. +// Without it, the linker would fail with "undefined reference to vtable". +#include "test_main_window.moc" diff --git a/docs/step1_gui_skeleton.md b/docs/step1_gui_skeleton.md new file mode 100644 index 0000000..4624a4d --- /dev/null +++ b/docs/step1_gui_skeleton.md @@ -0,0 +1,89 @@ +# Step 1 — GUI Skeleton + +## What Was Built + +The main window layout for the LIN Master Simulator using PyQt6 (Python). + +## Architecture + +``` +MainWindow (QMainWindow) +├── Menu Bar +│ ├── File → Load LDF... (Ctrl+O), Exit (Ctrl+Q) +│ └── View → Toggle Connection panel +├── LDF Toolbar (top, fixed) +│ ├── LDF File path (read-only QLineEdit) +│ ├── Browse button → opens file dialog +│ └── Auto-reload checkbox (default: on) +├── Central Widget +│ └── QSplitter (vertical, resizable) +│ ├── Tx Panel (QGroupBox) +│ │ └── QTableWidget: Frame Name | Frame ID | Length | Interval (ms) | Data | Signals | Action +│ └── Rx Panel (QGroupBox) +│ └── QTableWidget: Timestamp | Frame Name | Frame ID | Data | Signals +├── Connection Dock (QDockWidget, left side, detachable) +│ ├── Device dropdown + Refresh button +│ ├── Baud Rate label (read-only, auto-detected from LDF's LIN_speed field) +│ ├── Connect / Disconnect buttons +│ ├── Status label (red "Disconnected") +│ └── Device info label +├── Control Bar (bottom toolbar, fixed) +│ ├── Schedule table dropdown +│ ├── Global Rate (ms) spinbox (1-10000ms, default 50ms) +│ ├── Start / Stop / Pause buttons (disabled) +│ └── Send Selected Frame button (disabled) +└── Status Bar + ├── Temporary messages (left) + └── Connection indicator (right, permanent) +``` + +## Send Rate Hierarchy +When the scheduler runs, each frame's send interval is determined by priority: +1. **Per-frame interval** (Tx table "Interval (ms)" column) — highest priority, user override +2. **Global send rate** (Control bar spinbox) — default fallback for frames with no per-frame interval +3. **LDF schedule table** — auto-fills per-frame intervals when LDF is loaded + +## Key Concepts Introduced + +### PyQt6 Widget Tree +Every visible element is a QWidget. Widgets contain other widgets via layouts. +The root widget is QMainWindow which provides menu, toolbars, dock areas, and status bar for free. + +### Layouts +- **QVBoxLayout**: stacks widgets vertically +- **QHBoxLayout**: stacks widgets horizontally +- **QSplitter**: like a layout but with a draggable divider + +### Signals & Slots +Qt's event system. A signal (e.g., `button.clicked`) connects to a slot (any Python function). +Example: `self.btn_browse_ldf.clicked.connect(self._on_load_ldf)` + +### QDockWidget +A panel that can be dragged to any window edge, floated, or hidden. +Used for the Connection panel so users can maximize table space during monitoring. + +### Initial Widget States +Controls that require prerequisites (LDF loaded, device connected) start **disabled**. +This prevents users from triggering actions that would fail. We enable them as preconditions are met. + +## Files + +| File | Purpose | +|------|---------| +| `python/src/main.py` | Application entry point — creates QApplication, shows MainWindow | +| `python/src/main_window.py` | MainWindow class — all panel creation and layout | +| `python/tests/test_main_window.py` | 26 tests verifying widget tree structure and initial states | + +## Running + +```bash +# Launch the GUI +cd python/src && python main.py + +# Run tests +cd python && python -m pytest tests/test_main_window.py -v +``` + +## Test Results +- 32 tests, all passing +- Tests cover: window properties, menu bar, toolbar, Tx/Rx tables (incl. Interval column), connection dock (incl. baud rate label), control bar (incl. global rate), status bar diff --git a/python/requirements.txt b/python/requirements.txt new file mode 100644 index 0000000..ff2bfe0 --- /dev/null +++ b/python/requirements.txt @@ -0,0 +1,2 @@ +PyQt6>=6.5.0 +pytest>=7.0.0 diff --git a/python/src/__init__.py b/python/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/src/main.py b/python/src/main.py new file mode 100644 index 0000000..6fbfabc --- /dev/null +++ b/python/src/main.py @@ -0,0 +1,74 @@ +""" +main.py — Application entry point for the LIN Simulator. + +HOW A PyQt6 APPLICATION STARTS: +================================ + +Every Qt application needs exactly ONE QApplication instance. This object: + 1. Manages the event loop (listens for mouse clicks, key presses, timers, etc.) + 2. Holds application-wide settings (style, font, etc.) + 3. Routes events to the correct widget + +The event loop is the core of any GUI application: + + ┌─────────────────────────────┐ + │ app.exec() starts │ + │ the EVENT LOOP │ + │ │ + │ ┌───────────────────────┐ │ + │ │ Wait for event │◄─┼── Mouse click, key press, + │ │ (blocking) │ │ timer tick, signal, etc. + │ ├───────────────────────┤ │ + │ │ Dispatch event to │ │ + │ │ correct widget │──┼── Widget processes it + │ ├───────────────────────┤ │ + │ │ Widget emits signals │ │ + │ │ → connected slots run │ │ + │ └───────┬───────────────┘ │ + │ │ loop back │ + │ └──────────────────│ + │ │ + │ Loop exits when last │ + │ window is closed │ + └─────────────────────────────┘ + +sys.exit(app.exec()) ensures the process exit code matches +Qt's exit code (0 = normal, non-zero = error). +""" + +import sys +from pathlib import Path +from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QIcon +from main_window import MainWindow + + +def main(): + # Step 1: Create the application object. + # sys.argv is passed so Qt can process command-line arguments + # like --style, --platform, etc. + app = QApplication(sys.argv) + + # Optional: set application metadata (used by OS for window grouping, etc.) + app.setApplicationName("LIN Simulator") + app.setOrganizationName("TeqanyLogix LTD") + + # Set application icon — used in the taskbar, window title bar, and dock. + # Path resolves relative to this script's location: src/ → ../../resources/ + icon_path = Path(__file__).parent.parent.parent / "resources" / "logo.png" + if icon_path.exists(): + app.setWindowIcon(QIcon(str(icon_path))) + + # Step 2: Create and show the main window. + # show() makes it visible — without this call, the window exists but is hidden. + window = MainWindow() + window.show() + + # Step 3: Enter the event loop. + # This call BLOCKS until the application exits (all windows closed). + # sys.exit() ensures the process returns Qt's exit code to the OS. + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/python/src/main_window.py b/python/src/main_window.py new file mode 100644 index 0000000..a64a377 --- /dev/null +++ b/python/src/main_window.py @@ -0,0 +1,547 @@ +""" +main_window.py — The root window of the LIN Simulator. + +ARCHITECTURE OVERVIEW: +====================== +QMainWindow is Qt's specialized top-level window. Unlike a plain QWidget, +it has built-in support for: + - Menu bar (File, Edit, Help menus) + - Toolbars (icon buttons at the top) + - Dock widgets (panels that can float or snap to edges) + - Status bar (info text at the bottom) + - Central widget (the main content area) + +Our layout strategy: +┌─────────────────────────────────────────────────┐ +│ Menu Bar (File > Load LDF, Exit) │ +├─────────────────────────────────────────────────┤ +│ LDF Toolbar: [path field] [Browse] [Auto-reload indicator] │ +├──────────┬──────────────────────────────────────┤ +│ │ Tx Panel (QGroupBox) │ +│ Connect │ QTableWidget (frames/signals) │ +│ Panel │──────────────────────────────────────│ +│ (Dock) │ Rx Panel (QGroupBox) │ +│ │ QTableWidget (timestamp/frames) │ +├──────────┴──────────────────────────────────────┤ +│ Control Bar: [Start] [Stop] [Send] | Status │ +└─────────────────────────────────────────────────┘ + +The Connection Panel is a QDockWidget — meaning the user can: + - Drag it to any edge of the window + - Undock it to a floating window + - Close/reopen it from the View menu +This is useful because during normal operation you might want more +space for Tx/Rx tables and hide the connection panel. +""" + +from PyQt6.QtWidgets import ( + QMainWindow, # The root window class + QWidget, # Base class for all UI objects + QVBoxLayout, # Arranges widgets vertically (top to bottom) + QHBoxLayout, # Arranges widgets horizontally (left to right) + QSplitter, # Resizable divider between two widgets + QGroupBox, # Labeled border around a group of widgets + QTableWidget, # Table with rows and columns + QHeaderView, # Controls table header behavior (resize modes) + QDockWidget, # Detachable/dockable panel + QToolBar, # Row of tool buttons/widgets + QStatusBar, # Bottom info bar + QLabel, # Static text display + QLineEdit, # Single-line text input + QPushButton, # Clickable button + QComboBox, # Dropdown selector + QCheckBox, # Checkbox toggle + QSpinBox, # Numeric input with up/down arrows + QFileDialog, # OS-native file picker dialog + QMessageBox, # Modal dialog for messages (info, warning, about) +) +from PyQt6.QtCore import Qt, QSize +from PyQt6.QtGui import QAction + + +class MainWindow(QMainWindow): + """ + The main application window. + + QMainWindow provides the framework — we fill in the content by: + 1. Setting up the menu bar → _create_menu_bar() + 2. Setting up the LDF toolbar → _create_ldf_toolbar() + 3. Creating the central widget → _create_central_widget() + 4. Adding the connection dock → _create_connection_dock() + 5. Adding the control bar → _create_control_bar() + 6. Setting up the status bar → _create_status_bar() + """ + + def __init__(self): + # super().__init__() calls QMainWindow's constructor. + # This is required — Qt needs to initialize its internal C++ object. + super().__init__() + + # Window properties + self.setWindowTitle("LIN Simulator") + self.setMinimumSize(QSize(1024, 768)) # Minimum usable size + + # Build the UI in logical order + self._create_menu_bar() + self._create_ldf_toolbar() + self._create_central_widget() + self._create_connection_dock() + self._create_control_bar() + self._create_status_bar() + + # ─── Menu Bar ────────────────────────────────────────────────────── + + def _create_menu_bar(self): + """ + Menu bar sits at the very top of the window (or macOS system bar). + + QAction represents a single menu item. It can have: + - Text ("Load LDF...") + - Shortcut (Ctrl+O) + - A connected slot (function to call when triggered) + + We use QAction so the same action can appear in both the menu + AND a toolbar button, sharing the same shortcut and behavior. + """ + menu_bar = self.menuBar() # QMainWindow provides this built-in + + # ── File menu ── + file_menu = menu_bar.addMenu("&File") + # The '&' before 'F' makes it an Alt+F keyboard accelerator + + # Load LDF action + self.action_load_ldf = QAction("&Load LDF...", self) + self.action_load_ldf.setShortcut("Ctrl+O") + self.action_load_ldf.setStatusTip("Load a LIN Description File") + self.action_load_ldf.triggered.connect(self._on_load_ldf) + file_menu.addAction(self.action_load_ldf) + + file_menu.addSeparator() # Visual divider line + + # Exit action + action_exit = QAction("E&xit", self) + action_exit.setShortcut("Ctrl+Q") + action_exit.triggered.connect(self.close) # close() is built into QMainWindow + file_menu.addAction(action_exit) + + # ── View menu ── (will be used to toggle dock widget visibility) + self.view_menu = menu_bar.addMenu("&View") + + # ── Help menu ── + help_menu = menu_bar.addMenu("&Help") + + action_about = QAction("&About", self) + action_about.triggered.connect(self._on_about) + help_menu.addAction(action_about) + + # ─── LDF Toolbar ─────────────────────────────────────────────────── + + def _create_ldf_toolbar(self): + """ + Toolbar for LDF file management. + + QToolBar is a horizontal bar that can hold widgets (not just buttons). + We add a QLineEdit to show the file path and a browse button. + + The auto-reload checkbox uses QFileSystemWatcher (added in Step 2) + to detect when the LDF file changes on disk and automatically re-parse it. + """ + toolbar = QToolBar("LDF File") + toolbar.setMovable(False) # Keep it fixed at the top + self.addToolBar(toolbar) + + toolbar.addWidget(QLabel(" LDF File: ")) + + # Path display — read-only, shows the currently loaded file path + self.ldf_path_edit = QLineEdit() + self.ldf_path_edit.setReadOnly(True) + self.ldf_path_edit.setPlaceholderText("No LDF file loaded") + self.ldf_path_edit.setMinimumWidth(300) + toolbar.addWidget(self.ldf_path_edit) + + # Browse button — opens a file dialog + self.btn_browse_ldf = QPushButton("Browse...") + self.btn_browse_ldf.clicked.connect(self._on_load_ldf) + toolbar.addWidget(self.btn_browse_ldf) + + # Auto-reload toggle + self.chk_auto_reload = QCheckBox("Auto-reload") + self.chk_auto_reload.setChecked(True) # On by default + self.chk_auto_reload.setToolTip( + "Automatically reload the LDF file when it changes on disk" + ) + toolbar.addWidget(self.chk_auto_reload) + + # ─── Central Widget (Tx + Rx Panels) ────────────────────────────── + + def _create_central_widget(self): + """ + The central widget is the main content area of QMainWindow. + + We use a QSplitter to divide it into Tx (top) and Rx (bottom) panels. + QSplitter lets the user drag the divider to resize the panels. + + Layout hierarchy: + QWidget (central) + └── QVBoxLayout + └── QSplitter (vertical) + ├── QGroupBox "Tx Frames (Master → Slave)" + │ └── QVBoxLayout + │ └── QTableWidget + └── QGroupBox "Rx Frames (Slave → Master)" + └── QVBoxLayout + └── QTableWidget + """ + central = QWidget() + layout = QVBoxLayout(central) + # setContentsMargins(left, top, right, bottom) — padding around the layout + layout.setContentsMargins(4, 4, 4, 4) + + # QSplitter orientation: Vertical means the divider is horizontal + # (widgets stack top/bottom). This is a common source of confusion! + splitter = QSplitter(Qt.Orientation.Vertical) + + # ── Tx Panel ── + tx_group = QGroupBox("Tx Frames (Master → Slave)") + tx_layout = QVBoxLayout(tx_group) + self.tx_table = self._create_tx_table() + tx_layout.addWidget(self.tx_table) + splitter.addWidget(tx_group) + + # ── Rx Panel ── + rx_group = QGroupBox("Rx Frames (Slave → Master)") + rx_layout = QVBoxLayout(rx_group) + self.rx_table = self._create_rx_table() + rx_layout.addWidget(self.rx_table) + splitter.addWidget(rx_group) + + # Set initial size ratio: 50% Tx, 50% Rx + # setSizes() takes pixel heights — these are proportional when the window resizes + splitter.setSizes([400, 400]) + + layout.addWidget(splitter) + self.setCentralWidget(central) + + def _create_tx_table(self): + """ + Create the Tx (transmit) frames table. + + QTableWidget is a ready-to-use table. For our Tx panel: + - Frame Name: LIN frame identifier (e.g., "BCM_Command") + - Frame ID: Hex ID (e.g., 0x3C) + - Length: Data bytes count (1-8) + - Interval (ms): Per-frame send rate in milliseconds. Overrides the + global rate. Auto-filled from LDF schedule table + slot delays, but user can edit for testing. + - Data: Raw hex bytes (e.g., "FF 00 A3 00 00 00 00 00") + - Signals: Decoded signal summary (filled in Step 3) + - Action: Send button per row (filled in Step 3) + + QHeaderView.ResizeMode controls how columns resize: + - Stretch: fills available space (good for Data/Signals) + - ResizeToContents: fits the content width (good for ID/Length) + """ + table = QTableWidget() + table.setColumnCount(7) + table.setHorizontalHeaderLabels( + ["Frame Name", "Frame ID", "Length", "Interval (ms)", + "Data", "Signals", "Action"] + ) + + # Configure header resize behavior + header = table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) # Frame Name + header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # ID + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) # Length + header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Interval + header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch) # Data + header.setSectionResizeMode(5, QHeaderView.ResizeMode.Stretch) # Signals + header.setSectionResizeMode(6, QHeaderView.ResizeMode.ResizeToContents) # Action + + # Alternating row colors improve readability + table.setAlternatingRowColors(True) + + # Select entire rows, not individual cells + table.setSelectionBehavior( + QTableWidget.SelectionBehavior.SelectRows + ) + + return table + + def _create_rx_table(self): + """ + Create the Rx (receive) frames table. + + Similar to Tx but with a Timestamp column and no Action column. + The Timestamp shows when each frame was received — critical for + debugging LIN bus timing issues. + + In Step 4, this table will auto-scroll as new frames arrive + and support pausing/resuming the scroll. + """ + table = QTableWidget() + table.setColumnCount(5) + table.setHorizontalHeaderLabels( + ["Timestamp", "Frame Name", "Frame ID", "Data", "Signals"] + ) + + header = table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Timestamp + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Frame Name + header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) # ID + header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) # Data + header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch) # Signals + + table.setAlternatingRowColors(True) + table.setSelectionBehavior( + QTableWidget.SelectionBehavior.SelectRows + ) + + # Rx table is read-only — user can't edit received data + table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + + return table + + # ─── Connection Dock Widget ──────────────────────────────────────── + + def _create_connection_dock(self): + """ + Connection panel as a QDockWidget. + + QDockWidget is special — it can: + 1. Dock to any edge (left/right/top/bottom) of the main window + 2. Float as a separate window + 3. Be closed and reopened from the View menu + + This makes the UI flexible: during setup, the user needs the + connection panel visible. During monitoring, they can hide it + to maximize table space. + + Layout inside the dock: + QWidget (container) + └── QVBoxLayout + ├── QLabel "Device:" + ├── QHBoxLayout + │ ├── QComboBox (device list) + │ └── QPushButton "Refresh" + ├── QLabel "Baud Rate:" + ├── QComboBox (9600, 19200, 20000 + editable for custom) + ├── QHBoxLayout + │ ├── QPushButton "Connect" + │ └── QPushButton "Disconnect" + ├── QLabel "Status: Disconnected" + └── QLabel "Device Info: —" + """ + dock = QDockWidget("Connection", self) + + # Allow docking on left and right edges only + dock.setAllowedAreas( + Qt.DockWidgetArea.LeftDockWidgetArea + | Qt.DockWidgetArea.RightDockWidgetArea + ) + + # Build the dock's content + container = QWidget() + layout = QVBoxLayout(container) + + # ── Device selection ── + layout.addWidget(QLabel("Device:")) + + device_row = QHBoxLayout() + self.combo_device = QComboBox() + self.combo_device.setPlaceholderText("Select device...") + self.combo_device.setMinimumWidth(150) + device_row.addWidget(self.combo_device) + + self.btn_refresh = QPushButton("Refresh") + self.btn_refresh.setToolTip("Scan for connected BabyLIN devices") + device_row.addWidget(self.btn_refresh) + layout.addLayout(device_row) + + # ── Baud rate display (read-only) ── + # LIN bus speed is defined in the LDF file's LIN_speed field. + # This is NOT user-editable — it's extracted from the LDF when loaded + # (Step 2). The BabyLIN device must be configured to match this rate. + # Shows "—" until an LDF is loaded. + layout.addWidget(QLabel("Baud Rate:")) + self.lbl_baud_rate = QLabel("— (load LDF)") + self.lbl_baud_rate.setStyleSheet("font-weight: bold;") + self.lbl_baud_rate.setToolTip( + "LIN bus baud rate — automatically detected from the LDF file" + ) + layout.addWidget(self.lbl_baud_rate) + + # ── Connect / Disconnect buttons ── + btn_row = QHBoxLayout() + self.btn_connect = QPushButton("Connect") + self.btn_disconnect = QPushButton("Disconnect") + self.btn_disconnect.setEnabled(False) # Disabled until connected + btn_row.addWidget(self.btn_connect) + btn_row.addWidget(self.btn_disconnect) + layout.addLayout(btn_row) + + # ── Status display ── + self.lbl_conn_status = QLabel("Status: Disconnected") + self.lbl_conn_status.setStyleSheet("color: red; font-weight: bold;") + layout.addWidget(self.lbl_conn_status) + + # ── Device info ── + self.lbl_device_info = QLabel("Device Info: —") + layout.addWidget(self.lbl_device_info) + + # Push everything to the top, remaining space below + layout.addStretch() + + dock.setWidget(container) + + # Add dock to the LEFT side of the main window + self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock) + + # Add a toggle action to the View menu so user can show/hide this panel + self.view_menu.addAction(dock.toggleViewAction()) + + # ─── Control Bar (Bottom Toolbar) ────────────────────────────────── + + def _create_control_bar(self): + """ + Bottom toolbar with scheduler and manual send controls. + + We use addToolBar with Qt.ToolBarArea.BottomToolBarArea to place + this at the bottom of the window. It contains: + - Schedule table selector + - Global send rate (default interval for all frames) + - Start/Stop/Pause buttons for the scheduler + - A manual "Send Frame" button + + SEND RATE HIERARCHY: + 1. Per-frame interval (Tx table "Interval (ms)" column) — highest priority + 2. Global send rate (this toolbar's spinbox) — default fallback + 3. LDF schedule table slot delays — auto-fill source for per-frame + + In Step 7, the scheduler will use QTimer to periodically send + frames according to the selected schedule table from the LDF. + """ + toolbar = QToolBar("Controls") + toolbar.setMovable(False) + self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, toolbar) + + # ── Schedule table selector ── + toolbar.addWidget(QLabel(" Schedule: ")) + self.combo_schedule = QComboBox() + self.combo_schedule.setPlaceholderText("No schedule loaded") + self.combo_schedule.setMinimumWidth(200) + toolbar.addWidget(self.combo_schedule) + + toolbar.addSeparator() + + # ── Global send rate ── + # QSpinBox provides a numeric input with up/down arrows and + # min/max/step constraints. This sets the default interval (in ms) + # between frame transmissions. Individual frames can override this + # via the "Interval (ms)" column in the Tx table. + # + # Range: 1ms to 10000ms (10 seconds) + # - 10ms = 100 frames/sec (aggressive, for stress testing) + # - 50ms = 20 frames/sec (typical LIN schedule) + # - 100ms = 10 frames/sec (relaxed) + toolbar.addWidget(QLabel(" Global Rate (ms): ")) + self.spin_global_rate = QSpinBox() + self.spin_global_rate.setRange(1, 10000) + self.spin_global_rate.setValue(50) # 50ms default = 20 frames/sec + self.spin_global_rate.setSingleStep(10) # Up/down arrows change by 10ms + self.spin_global_rate.setSuffix(" ms") # Display "50 ms" in the box + self.spin_global_rate.setToolTip( + "Default send interval for all frames. " + "Per-frame intervals in the Tx table override this." + ) + toolbar.addWidget(self.spin_global_rate) + + toolbar.addSeparator() + + # ── Scheduler controls ── + self.btn_start = QPushButton("▶ Start") + self.btn_stop = QPushButton("■ Stop") + self.btn_pause = QPushButton("⏸ Pause") + + # Initially disabled — enabled when LDF is loaded and device connected + self.btn_start.setEnabled(False) + self.btn_stop.setEnabled(False) + self.btn_pause.setEnabled(False) + + toolbar.addWidget(self.btn_start) + toolbar.addWidget(self.btn_stop) + toolbar.addWidget(self.btn_pause) + + toolbar.addSeparator() + + # ── Manual send ── + self.btn_manual_send = QPushButton("Send Selected Frame") + self.btn_manual_send.setEnabled(False) + toolbar.addWidget(self.btn_manual_send) + + # ─── Status Bar ──────────────────────────────────────────────────── + + def _create_status_bar(self): + """ + Status bar at the very bottom of the window. + + QStatusBar can show: + - Temporary messages (disappear after a timeout) + - Permanent widgets (always visible, e.g., connection indicator) + + We add a permanent label on the right side for the connection state, + and use showMessage() for transient feedback like "LDF loaded successfully". + """ + status_bar = QStatusBar() + self.setStatusBar(status_bar) + + # Permanent connection status on the right side + self.lbl_status_connection = QLabel("● Disconnected") + self.lbl_status_connection.setStyleSheet("color: red;") + status_bar.addPermanentWidget(self.lbl_status_connection) + + # Show initial message + status_bar.showMessage("Ready — Load an LDF file to begin", 5000) + # 5000 = message disappears after 5 seconds + + # ─── Slot: Load LDF ─────────────────────────────────────────────── + + def _on_load_ldf(self): + """ + Slot connected to the Load LDF action and Browse button. + + QFileDialog.getOpenFileName() opens the OS-native file picker. + It returns a tuple: (file_path, selected_filter). + If the user cancels, file_path is an empty string. + + The actual parsing happens in Step 2 — for now we just show + the selected path. + """ + file_path, _ = QFileDialog.getOpenFileName( + self, # Parent window + "Open LIN Description File", # Dialog title + "", # Starting directory (empty = last used) + "LDF Files (*.ldf);;All Files (*)" # File type filters + ) + + if file_path: + self.ldf_path_edit.setText(file_path) + self.statusBar().showMessage(f"LDF file selected: {file_path}", 3000) + + # ─── Slot: About ────────────────────────────────────────────────── + + def _on_about(self): + """Show the About dialog with ownership and developer information.""" + QMessageBox.about( + self, + "About LIN Simulator", + "

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.

" + ) diff --git a/python/tests/__init__.py b/python/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/test_main_window.py b/python/tests/test_main_window.py new file mode 100644 index 0000000..8840c47 --- /dev/null +++ b/python/tests/test_main_window.py @@ -0,0 +1,207 @@ +""" +test_main_window.py — Tests for the GUI skeleton (Step 1). + +TESTING PyQt6 APPLICATIONS: +============================ +GUI testing has a challenge: Qt widgets need a running QApplication. +We use a pytest fixture to create ONE QApplication for the entire test session. + +Key things we verify: + 1. Window creates without errors + 2. All panels exist and are the correct widget types + 3. Tables have the right columns + 4. Buttons and controls are in the expected initial state + 5. Window has a reasonable minimum size + +We do NOT test visual appearance (that's manual testing). +We DO test that the widget tree is correctly assembled. +""" + +import sys +import pytest + +# Add src to path so we can import our modules +sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent.parent / "src")) + +from PyQt6.QtWidgets import QApplication, QDockWidget, QTableWidget +from PyQt6.QtCore import Qt + + +# ─── Fixtures ────────────────────────────────────────────────────────── +# Fixtures are pytest's way of providing shared setup. The "session" scope +# means this QApplication is created once and reused across ALL tests. + +@pytest.fixture(scope="session") +def app(): + """Create a QApplication instance for the test session.""" + application = QApplication.instance() or QApplication(sys.argv) + yield application + + +@pytest.fixture +def window(app): + """Create a fresh MainWindow for each test.""" + from main_window import MainWindow + w = MainWindow() + yield w + w.close() + + +# ─── Tests ───────────────────────────────────────────────────────────── + +class TestWindowBasics: + """Test that the window initializes correctly.""" + + def test_window_title(self, window): + assert window.windowTitle() == "LIN Simulator" + + def test_minimum_size(self, window): + assert window.minimumWidth() >= 1024 + assert window.minimumHeight() >= 768 + + def test_central_widget_exists(self, window): + assert window.centralWidget() is not None + + +class TestMenuBar: + """Test menu bar structure.""" + + def test_menu_bar_exists(self, window): + assert window.menuBar() is not None + + def test_load_ldf_action_exists(self, window): + assert window.action_load_ldf is not None + assert window.action_load_ldf.text() == "&Load LDF..." + + def test_load_ldf_shortcut(self, window): + assert window.action_load_ldf.shortcut().toString() == "Ctrl+O" + + +class TestLdfToolbar: + """Test the LDF file toolbar.""" + + def test_ldf_path_field_exists(self, window): + assert window.ldf_path_edit is not None + assert window.ldf_path_edit.isReadOnly() + + def test_ldf_path_placeholder(self, window): + assert window.ldf_path_edit.placeholderText() == "No LDF file loaded" + + def test_browse_button_exists(self, window): + assert window.btn_browse_ldf is not None + + def test_auto_reload_default_checked(self, window): + assert window.chk_auto_reload.isChecked() + + +class TestTxTable: + """Test the Tx (transmit) table structure.""" + + def test_tx_table_exists(self, window): + assert isinstance(window.tx_table, QTableWidget) + + def test_tx_table_columns(self, window): + assert window.tx_table.columnCount() == 7 + headers = [] + for i in range(window.tx_table.columnCount()): + headers.append(window.tx_table.horizontalHeaderItem(i).text()) + assert headers == [ + "Frame Name", "Frame ID", "Length", "Interval (ms)", + "Data", "Signals", "Action" + ] + + def test_tx_table_alternating_colors(self, window): + assert window.tx_table.alternatingRowColors() + + +class TestRxTable: + """Test the Rx (receive) table structure.""" + + def test_rx_table_exists(self, window): + assert isinstance(window.rx_table, QTableWidget) + + def test_rx_table_columns(self, window): + assert window.rx_table.columnCount() == 5 + headers = [] + for i in range(window.rx_table.columnCount()): + headers.append(window.rx_table.horizontalHeaderItem(i).text()) + assert headers == ["Timestamp", "Frame Name", "Frame ID", "Data", "Signals"] + + def test_rx_table_not_editable(self, window): + """Rx table should be read-only — users can't edit received data.""" + assert ( + window.rx_table.editTriggers() + == QTableWidget.EditTrigger.NoEditTriggers + ) + + +class TestConnectionDock: + """Test the connection panel dock widget.""" + + def test_dock_exists(self, window): + docks = window.findChildren(QDockWidget) + assert len(docks) == 1 + assert docks[0].windowTitle() == "Connection" + + def test_device_combo_exists(self, window): + assert window.combo_device is not None + + def test_connect_button_exists(self, window): + assert window.btn_connect is not None + assert window.btn_connect.isEnabled() + + def test_disconnect_button_disabled_initially(self, window): + """Disconnect should be disabled when no device is connected.""" + assert not window.btn_disconnect.isEnabled() + + def test_status_label_shows_disconnected(self, window): + assert "Disconnected" in window.lbl_conn_status.text() + + def test_baud_rate_label_exists(self, window): + assert window.lbl_baud_rate is not None + + def test_baud_rate_shows_placeholder_before_ldf(self, window): + """Before loading an LDF, baud rate should show a placeholder.""" + assert "load LDF" in window.lbl_baud_rate.text() + + +class TestControlBar: + """Test the bottom control toolbar.""" + + def test_schedule_combo_exists(self, window): + assert window.combo_schedule is not None + + def test_scheduler_buttons_disabled_initially(self, window): + """Start/Stop/Pause should be disabled until LDF loaded + connected.""" + assert not window.btn_start.isEnabled() + assert not window.btn_stop.isEnabled() + assert not window.btn_pause.isEnabled() + + def test_manual_send_disabled_initially(self, window): + assert not window.btn_manual_send.isEnabled() + + def test_global_rate_spinbox_exists(self, window): + assert window.spin_global_rate is not None + + def test_global_rate_default_50ms(self, window): + """50ms default = ~20 frames/sec, typical LIN schedule speed.""" + assert window.spin_global_rate.value() == 50 + + def test_global_rate_range(self, window): + """Range should be 1ms (fast stress test) to 10000ms (very slow).""" + assert window.spin_global_rate.minimum() == 1 + assert window.spin_global_rate.maximum() == 10000 + + def test_global_rate_suffix(self, window): + """Should display the unit 'ms' in the spinbox.""" + assert window.spin_global_rate.suffix() == " ms" + + +class TestStatusBar: + """Test the status bar.""" + + def test_status_bar_exists(self, window): + assert window.statusBar() is not None + + def test_connection_status_label(self, window): + assert "Disconnected" in window.lbl_status_connection.text() diff --git a/resources/logo.png b/resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c7de099b2c264d743b8006942aa9345115959673 GIT binary patch literal 77417 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

aUpkFGV%h0 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 SlotZvpSr4!N(ss; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LIN + + + SIMULATOR + + + + + + + + + Tx + Rx +