commit b80877057382af2eec24c8f5a8f72bdcbd310950 Author: Mohamed Salem Date: Thu Apr 2 16:40:52 2026 +0200 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) 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 0000000..c7de099 Binary files /dev/null and b/resources/logo.png differ diff --git a/resources/logo.svg b/resources/logo.svg new file mode 100644 index 0000000..168e5c7 --- /dev/null +++ b/resources/logo.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LIN + + + SIMULATOR + + + + + + + + + Tx + Rx +