From c6f0d2fdde7583eb163e2160bf5ee77dffbdae3f Mon Sep 17 00:00:00 2001 From: Mohamed Salem Date: Wed, 8 Apr 2026 03:28:02 +0200 Subject: [PATCH] Add README, automotive dark theme, and polished GUI - README.md with full project documentation, architecture, setup guide - Automotive/industrial dark theme (theme.py) inspired by Vector CANoe - Dark blue-black background with amber/orange accents - Styled buttons (green Start, red Stop), orange hover effects - Custom scrollbars, tooltips, group box titles, tree widget headers - Updated highlight colors to match theme palette Co-Authored-By: Claude Opus 4.6 (1M context) --- PLAN.md | 3 +- README.md | 186 ++++++++++++ python/src/main.py | 9 +- python/src/main_window.py | 25 +- python/src/theme.py | 476 +++++++++++++++++++++++++++++++ python/tests/test_rx_realtime.py | 6 +- 6 files changed, 686 insertions(+), 19 deletions(-) create mode 100644 README.md create mode 100644 python/src/theme.py diff --git a/PLAN.md b/PLAN.md index a79af17..c6f75cb 100644 --- a/PLAN.md +++ b/PLAN.md @@ -190,7 +190,8 @@ LIN_Control_Tool/ --- ### Step 8 — Integration & End-to-End -- **Status:** In progress +- **Status:** DONE — Python (182 tests) | C++ (124 tests) +- **Features:** BabyLinBackend wired to GUI, global rate live update, full workflow tests, edge case handling - **Goal:** Wire all components together, full workflow testing. **Features:** diff --git a/README.md b/README.md new file mode 100644 index 0000000..89a61c5 --- /dev/null +++ b/README.md @@ -0,0 +1,186 @@ +# LIN Simulator + +A cross-platform GUI tool for simulating LIN master nodes using **BabyLIN-RC-II** devices by Lipowsky. Built in two parallel implementations — **Python (PyQt6)** and **C++ (Qt6)** — maintained in feature parity. + +**Owner:** TeqanyLogix LTD +**Developer:** Mohamed Salem + +--- + +## Features + +- **LDF Loading** — Parse LIN Description Files, auto-reload on file change +- **Tx Panel** — Expandable tree view of master frames with editable signal values +- **Rx Panel** — Real-time slave frame display with timestamps and change highlighting +- **Bit Packing** — Signal value edits automatically update frame bytes (and vice versa) +- **Hex/Dec Toggle** — Switch all values between hexadecimal and decimal display +- **Schedule Tables** — Select and run LDF schedule tables with start/stop/pause +- **Mock Rx Simulation** — Simulated slave responses for testing without hardware +- **Connection Panel** — Serial port discovery, connect/disconnect with status indicator +- **BabyLIN Backend** — Wraps Lipowsky's BabyLIN DLL for hardware communication +- **Baud Rate Detection** — Automatically extracted from LDF's LIN_speed field +- **FreeFormat Entries** — Support for raw diagnostic/configuration schedule entries +- **Automotive Dark Theme** — Industrial UI inspired by Vector CANoe and ETAS INCA + +## Screenshots + +*(Launch the app and load an LDF to see the automotive dark theme in action)* + +## Project Structure + +``` +LIN_Control_Tool/ +├── python/ # Python implementation +│ ├── src/ +│ │ ├── main.py # Entry point +│ │ ├── main_window.py # GUI layout and logic +│ │ ├── ldf_handler.py # LDF parsing adapter (wraps ldfparser) +│ │ ├── connection_manager.py # Serial port state machine +│ │ ├── babylin_backend.py # BabyLIN DLL wrapper +│ │ ├── scheduler.py # QTimer-based schedule execution +│ │ └── theme.py # Automotive dark theme stylesheet +│ ├── tests/ # pytest test suite (182 tests) +│ └── requirements.txt +├── cpp/ # C++ implementation +│ ├── src/ +│ │ ├── main.cpp # Entry point +│ │ ├── main_window.h/.cpp # GUI layout and logic +│ │ ├── ldf_parser.h/.cpp # Custom LDF parser (regex-based) +│ │ ├── connection_manager.h/.cpp # QSerialPort state machine +│ │ └── scheduler.h/.cpp # QTimer-based schedule execution +│ ├── tests/ # QTest test suite (124 tests) +│ └── CMakeLists.txt +├── resources/ +│ ├── sample.ldf # Sample LIN 2.1 LDF for testing +│ ├── logo.svg # Application logo (SVG source) +│ └── logo.png # Application logo (512x512 PNG) +└── docs/ # Step-by-step documentation +``` + +## Quick Start + +### Python + +```bash +cd python +pip install -r requirements.txt +cd src && python main.py +``` + +### C++ + +```bash +cd cpp +mkdir build && cd build +cmake .. -DCMAKE_PREFIX_PATH=$(brew --prefix qt@6) # macOS +# cmake .. # Linux/Windows +cmake --build . +./lin_simulator +``` + +## Running Tests + +### Python (pytest) + +```bash +cd python +python -m pytest tests/ -v # all tests +python -m pytest tests/test_ldf_handler.py -v # single file +``` + +### C++ (QTest / CTest) + +```bash +cd cpp/build +ctest --output-on-failure -V # all tests +./test_main_window # single test executable +``` + +## Dependencies + +### Python +| Package | Version | Purpose | +|---------|---------|---------| +| PyQt6 | >= 6.5.0 | GUI framework | +| ldfparser | >= 0.25.0 | LDF file parsing | +| pyserial | >= 3.5 | Serial port communication | +| pytest | >= 7.0.0 | Test framework | + +### C++ +| Library | Purpose | +|---------|---------| +| Qt6 Widgets | GUI framework | +| Qt6 SerialPort | Serial port communication | +| Qt6 Test | Test framework | +| CMake >= 3.16 | Build system | + +## BabyLIN Hardware Support + +The tool communicates with **BabyLIN-RC-II** devices using Lipowsky's official BabyLIN DLL. + +| Platform | Status | +|----------|--------| +| **Linux** | Full hardware support via `libBabyLIN.so` | +| **Windows** | Full hardware support via `BabyLIN.dll` | +| **macOS** | Mock mode (development/testing only — no native DLL) | + +### Hardware Setup + +1. Install LinWorks from Lipowsky +2. Compile your LDF into an SDF file using LinWorks +3. Connect the BabyLIN-RC-II via USB +4. Launch the LIN Simulator → Refresh → Connect → Load SDF → Start + +### Mock Mode + +When the BabyLIN DLL is not available, the tool operates in **mock mode**: +- All GUI features work normally +- The scheduler generates simulated Rx data +- Useful for GUI development and testing without hardware + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ MainWindow (GUI) │ +│ ┌──────────┐ ┌──────────────────────────┐ ┌──────────────────┐ │ +│ │Connection│ │ Tx Tree │ Rx Tree │ │ Control Bar │ │ +│ │ Panel │ │ (editable) │ (live) │ │ Start/Stop/Pause│ │ +│ └────┬─────┘ └──────┬─────┴──────┬──────┘ └────────┬─────────┘ │ +│ │ │ │ │ │ +├───────┴───────────────┴────────────┴───────────────────┴────────────┤ +│ Application Logic │ +│ ┌────────────────┐ ┌─────────────┐ ┌────────────────────────┐ │ +│ │ LDF Handler │ │ Scheduler │ │ Connection Manager │ │ +│ │ (parse LDF) │ │ (QTimer) │ │ (serial port state) │ │ +│ └────────────────┘ └──────┬──────┘ └───────────┬────────────┘ │ +│ │ │ │ +├─────────────────────────────┴─────────────────────┴─────────────────┤ +│ Hardware Layer │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ BabyLIN Backend (wraps Lipowsky DLL / mock mode) │ │ +│ │ → libBabyLIN.so (Linux) / BabyLIN.dll (Windows) │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## LDF File Support + +The tool parses standard LIN 2.0/2.1 LDF files and extracts: +- Protocol/language version and baud rate +- Master and slave node definitions +- Frame definitions with signal mappings (bit offset, width, initial value) +- Schedule tables (including FreeFormat diagnostic entries) +- Node attributes + +### Sample LDF + +A test LDF (`resources/sample.ldf`) is included with: +- 1 master node (ECU_Master), 2 slave nodes +- 4 frames (2 Tx, 2 Rx), 9 signals +- 2 schedule tables (NormalSchedule, FastSchedule) +- 19200 baud + +## License + +Copyright 2026 TeqanyLogix LTD. All rights reserved. diff --git a/python/src/main.py b/python/src/main.py index 6fbfabc..d8c4758 100644 --- a/python/src/main.py +++ b/python/src/main.py @@ -39,8 +39,9 @@ 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 PyQt6.QtGui import QIcon, QFont from main_window import MainWindow +from theme import STYLESHEET def main(): @@ -53,12 +54,14 @@ def main(): 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/ + # Set application icon icon_path = Path(__file__).parent.parent.parent / "resources" / "logo.png" if icon_path.exists(): app.setWindowIcon(QIcon(str(icon_path))) + # Apply automotive/industrial dark theme + app.setStyleSheet(STYLESHEET) + # Step 2: Create and show the main window. # show() makes it visible — without this call, the window exists but is hidden. window = MainWindow() diff --git a/python/src/main_window.py b/python/src/main_window.py index e1cbdf0..9282927 100644 --- a/python/src/main_window.py +++ b/python/src/main_window.py @@ -127,7 +127,8 @@ Our layout strategy: The Connection Panel is a QDockWidget — meaning the user can: - Drag it to any edge of the window - - Undock it to a floating window + - Undock it + 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. @@ -593,7 +594,7 @@ class MainWindow(QMainWindow): # ── Status display ── self.lbl_conn_status = QLabel("Status: Disconnected") - self.lbl_conn_status.setStyleSheet("color: red; font-weight: bold;") + self.lbl_conn_status.setStyleSheet("color: #e74c3c; font-weight: bold;") layout.addWidget(self.lbl_conn_status) # ── Device info ── @@ -714,7 +715,7 @@ class MainWindow(QMainWindow): # Permanent connection status on the right side self.lbl_status_connection = QLabel("● Disconnected") - self.lbl_status_connection.setStyleSheet("color: red;") + self.lbl_status_connection.setStyleSheet("color: #e74c3c;") status_bar.addPermanentWidget(self.lbl_status_connection) # Show initial message @@ -1428,7 +1429,7 @@ class MainWindow(QMainWindow): old_val = prev_values.get(sig_info.name) if old_val is not None and old_val != value: # Yellow background for changed signals - sig_item.setBackground(4, QBrush(QColor(255, 255, 100))) + sig_item.setBackground(4, QBrush(QColor(241, 196, 15, 100))) else: # Reset background sig_item.setBackground(4, QBrush()) @@ -1593,7 +1594,7 @@ class MainWindow(QMainWindow): if item.text(0) == frame_name: # Light blue background for currently transmitting frame for col in range(tree.columnCount()): - item.setBackground(col, QBrush(QColor(173, 216, 230))) + item.setBackground(col, QBrush(QColor(230, 126, 34, 60))) break def _clear_frame_highlight(self): @@ -1707,17 +1708,17 @@ class MainWindow(QMainWindow): if state == ConnectionState.DISCONNECTED: self.lbl_conn_status.setText("Status: Disconnected") - self.lbl_conn_status.setStyleSheet("color: red; font-weight: bold;") + self.lbl_conn_status.setStyleSheet("color: #e74c3c; font-weight: bold;") self.btn_connect.setEnabled(True) self.btn_disconnect.setEnabled(False) self.lbl_device_info.setText("Device Info: —") self.lbl_status_connection.setText("● Disconnected") - self.lbl_status_connection.setStyleSheet("color: red;") + self.lbl_status_connection.setStyleSheet("color: #e74c3c;") elif state == ConnectionState.CONNECTING: self.lbl_conn_status.setText("Status: Connecting...") self.lbl_conn_status.setStyleSheet( - "color: orange; font-weight: bold;" + "color: #e67e22; font-weight: bold;" ) self.btn_connect.setEnabled(False) self.btn_disconnect.setEnabled(False) @@ -1726,7 +1727,7 @@ class MainWindow(QMainWindow): port_info = self._conn_mgr.connected_port self.lbl_conn_status.setText("Status: Connected") self.lbl_conn_status.setStyleSheet( - "color: green; font-weight: bold;" + "color: #27ae60; font-weight: bold;" ) self.btn_connect.setEnabled(False) self.btn_disconnect.setEnabled(True) @@ -1736,17 +1737,17 @@ class MainWindow(QMainWindow): f"{port_info.description}" ) self.lbl_status_connection.setText("● Connected") - self.lbl_status_connection.setStyleSheet("color: green;") + self.lbl_status_connection.setStyleSheet("color: #27ae60;") elif state == ConnectionState.ERROR: self.lbl_conn_status.setText( f"Status: Error\n{self._conn_mgr.error_message}" ) - self.lbl_conn_status.setStyleSheet("color: red; font-weight: bold;") + self.lbl_conn_status.setStyleSheet("color: #e74c3c; font-weight: bold;") self.btn_connect.setEnabled(True) self.btn_disconnect.setEnabled(False) self.lbl_status_connection.setText("● Error") - self.lbl_status_connection.setStyleSheet("color: red;") + self.lbl_status_connection.setStyleSheet("color: #e74c3c;") # ─── Slot: About ────────────────────────────────────────────────── diff --git a/python/src/theme.py b/python/src/theme.py new file mode 100644 index 0000000..c74344d --- /dev/null +++ b/python/src/theme.py @@ -0,0 +1,476 @@ +""" +theme.py — Automotive/Industrial dark theme for the LIN Simulator. + +Inspired by Vector CANoe, ETAS INCA, and automotive diagnostic tools. +Dark background with amber/orange accents for an industrial look. + +QT STYLESHEETS: +=============== +Qt supports CSS-like stylesheets for customizing widget appearance. +The syntax is similar to web CSS but with Qt-specific selectors: + + QPushButton { ← applies to ALL QPushButtons + background-color: #333; + color: white; + border-radius: 4px; + } + + QPushButton:hover { ← when mouse hovers over button + background-color: #555; + } + + QPushButton#myButton { ← specific widget with objectName "myButton" + background-color: red; + } + + QTreeWidget::item { ← items inside the tree widget + padding: 4px; + } + +This is how professional Qt applications get their polished look +without creating custom paint code for every widget. +""" + +# ── Color Palette ───────────────────────────────────────────────────── +# Automotive/industrial color scheme + +COLORS = { + # Base colors + "bg_dark": "#1a1a2e", # Main background (very dark blue-black) + "bg_medium": "#16213e", # Panel backgrounds + "bg_light": "#0f3460", # Lighter panels, selected items + "bg_input": "#1c2541", # Input field backgrounds + + # Accent colors (amber/orange — automotive instrument cluster feel) + "accent": "#e67e22", # Primary accent (amber/orange) + "accent_hover": "#f39c12", # Hover state (brighter amber) + "accent_dim": "#d35400", # Pressed/active state + + # Status colors + "green": "#27ae60", # Connected, success + "red": "#e74c3c", # Disconnected, error + "yellow": "#f1c40f", # Warning, change highlight + "blue": "#3498db", # Info, frame highlight + + # Text + "text": "#ecf0f1", # Primary text (light gray) + "text_dim": "#95a5a6", # Secondary text (dimmer) + "text_dark": "#2c3e50", # Text on light backgrounds + + # Borders + "border": "#2c3e50", # Subtle borders + "border_light": "#34495e", # Lighter borders +} + +# ── Main Stylesheet ─────────────────────────────────────────────────── + +STYLESHEET = f""" + +/* ─── Global ────────────────────────────────────────────────────── */ + +QMainWindow {{ + background-color: {COLORS['bg_dark']}; +}} + +QWidget {{ + background-color: {COLORS['bg_dark']}; + color: {COLORS['text']}; + font-family: "Segoe UI", "SF Pro Display", "Helvetica Neue", Arial, sans-serif; + font-size: 13px; +}} + +/* ─── Menu Bar ──────────────────────────────────────────────────── */ + +QMenuBar {{ + background-color: {COLORS['bg_medium']}; + color: {COLORS['text']}; + border-bottom: 1px solid {COLORS['border']}; + padding: 2px; +}} + +QMenuBar::item {{ + padding: 6px 12px; + border-radius: 4px; +}} + +QMenuBar::item:selected {{ + background-color: {COLORS['accent']}; + color: white; +}} + +QMenu {{ + background-color: {COLORS['bg_medium']}; + color: {COLORS['text']}; + border: 1px solid {COLORS['border']}; + padding: 4px; +}} + +QMenu::item {{ + padding: 6px 24px; + border-radius: 3px; +}} + +QMenu::item:selected {{ + background-color: {COLORS['accent']}; + color: white; +}} + +QMenu::separator {{ + height: 1px; + background-color: {COLORS['border']}; + margin: 4px 8px; +}} + +/* ─── Toolbars ──────────────────────────────────────────────────── */ + +QToolBar {{ + background-color: {COLORS['bg_medium']}; + border: 1px solid {COLORS['border']}; + padding: 4px; + spacing: 6px; +}} + +QToolBar QLabel {{ + color: {COLORS['text_dim']}; + font-weight: bold; + font-size: 12px; +}} + +QToolBar::separator {{ + width: 1px; + background-color: {COLORS['border_light']}; + margin: 4px 6px; +}} + +/* ─── Buttons ───────────────────────────────────────────────────── */ + +QPushButton {{ + background-color: {COLORS['bg_light']}; + color: {COLORS['text']}; + border: 1px solid {COLORS['border_light']}; + border-radius: 5px; + padding: 6px 16px; + font-weight: bold; + min-height: 24px; +}} + +QPushButton:hover {{ + background-color: {COLORS['accent']}; + border-color: {COLORS['accent']}; + color: white; +}} + +QPushButton:pressed {{ + background-color: {COLORS['accent_dim']}; +}} + +QPushButton:disabled {{ + background-color: {COLORS['bg_dark']}; + color: {COLORS['text_dim']}; + border-color: {COLORS['border']}; +}} + +/* Start button — green accent */ +QPushButton[text*="Start"] {{ + background-color: {COLORS['green']}; + color: white; + border-color: {COLORS['green']}; +}} + +QPushButton[text*="Start"]:hover {{ + background-color: #2ecc71; +}} + +QPushButton[text*="Start"]:disabled {{ + background-color: {COLORS['bg_dark']}; + color: {COLORS['text_dim']}; + border-color: {COLORS['border']}; +}} + +/* Stop button — red accent */ +QPushButton[text*="Stop"] {{ + background-color: {COLORS['red']}; + color: white; + border-color: {COLORS['red']}; +}} + +QPushButton[text*="Stop"]:hover {{ + background-color: #c0392b; +}} + +QPushButton[text*="Stop"]:disabled {{ + background-color: {COLORS['bg_dark']}; + color: {COLORS['text_dim']}; + border-color: {COLORS['border']}; +}} + +/* ─── Input Fields ──────────────────────────────────────────────── */ + +QLineEdit {{ + background-color: {COLORS['bg_input']}; + color: {COLORS['text']}; + border: 1px solid {COLORS['border_light']}; + border-radius: 4px; + padding: 5px 8px; + selection-background-color: {COLORS['accent']}; +}} + +QLineEdit:read-only {{ + background-color: {COLORS['bg_dark']}; + color: {COLORS['text_dim']}; +}} + +QSpinBox {{ + background-color: {COLORS['bg_input']}; + color: {COLORS['accent']}; + border: 1px solid {COLORS['border_light']}; + border-radius: 4px; + padding: 4px 8px; + font-weight: bold; + font-size: 14px; +}} + +QSpinBox::up-button, QSpinBox::down-button {{ + background-color: {COLORS['bg_light']}; + border: none; + width: 20px; +}} + +QSpinBox::up-button:hover, QSpinBox::down-button:hover {{ + background-color: {COLORS['accent']}; +}} + +/* ─── Combo Boxes ───────────────────────────────────────────────── */ + +QComboBox {{ + background-color: {COLORS['bg_input']}; + color: {COLORS['text']}; + border: 1px solid {COLORS['border_light']}; + border-radius: 4px; + padding: 5px 8px; + min-height: 24px; +}} + +QComboBox:hover {{ + border-color: {COLORS['accent']}; +}} + +QComboBox::drop-down {{ + background-color: {COLORS['bg_light']}; + border: none; + width: 24px; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +}} + +QComboBox QAbstractItemView {{ + background-color: {COLORS['bg_medium']}; + color: {COLORS['text']}; + border: 1px solid {COLORS['border']}; + selection-background-color: {COLORS['accent']}; + selection-color: white; +}} + +/* ─── Check Boxes ───────────────────────────────────────────────── */ + +QCheckBox {{ + color: {COLORS['text']}; + spacing: 6px; +}} + +QCheckBox::indicator {{ + width: 18px; + height: 18px; + border: 2px solid {COLORS['border_light']}; + border-radius: 3px; + background-color: {COLORS['bg_input']}; +}} + +QCheckBox::indicator:checked {{ + background-color: {COLORS['accent']}; + border-color: {COLORS['accent']}; +}} + +QCheckBox::indicator:hover {{ + border-color: {COLORS['accent']}; +}} + +/* ─── Group Boxes ───────────────────────────────────────────────── */ + +QGroupBox {{ + background-color: {COLORS['bg_medium']}; + border: 1px solid {COLORS['border']}; + border-radius: 6px; + margin-top: 12px; + padding-top: 16px; + font-weight: bold; + font-size: 13px; +}} + +QGroupBox::title {{ + subcontrol-origin: margin; + subcontrol-position: top left; + padding: 4px 12px; + background-color: {COLORS['accent']}; + color: white; + border-radius: 4px; + margin-left: 8px; +}} + +/* ─── Tree Widget (Tx/Rx Tables) ────────────────────────────────── */ + +QTreeWidget {{ + background-color: {COLORS['bg_dark']}; + alternate-background-color: {COLORS['bg_medium']}; + color: {COLORS['text']}; + border: 1px solid {COLORS['border']}; + border-radius: 4px; + gridline-color: {COLORS['border']}; + outline: none; +}} + +QTreeWidget::item {{ + padding: 4px 2px; + border-bottom: 1px solid {COLORS['border']}; + min-height: 24px; +}} + +QTreeWidget::item:selected {{ + background-color: {COLORS['bg_light']}; + color: {COLORS['accent']}; +}} + +QTreeWidget::item:hover {{ + background-color: rgba(230, 126, 34, 0.15); +}} + +QTreeWidget::branch {{ + background-color: transparent; +}} + +/* Header for tree/table */ +QHeaderView::section {{ + background-color: {COLORS['bg_medium']}; + color: {COLORS['accent']}; + border: 1px solid {COLORS['border']}; + padding: 6px 8px; + font-weight: bold; + font-size: 12px; + text-transform: uppercase; +}} + +/* ─── Dock Widget ───────────────────────────────────────────────── */ + +QDockWidget {{ + color: {COLORS['text']}; + font-weight: bold; + titlebar-close-icon: none; +}} + +QDockWidget::title {{ + background-color: {COLORS['accent']}; + color: white; + padding: 8px; + border-radius: 0px; + font-size: 13px; +}} + +QDockWidget > QWidget {{ + background-color: {COLORS['bg_medium']}; + border: 1px solid {COLORS['border']}; +}} + +/* ─── Status Bar ────────────────────────────────────────────────── */ + +QStatusBar {{ + background-color: {COLORS['bg_medium']}; + color: {COLORS['text_dim']}; + border-top: 1px solid {COLORS['border']}; + font-size: 12px; + padding: 2px; +}} + +QStatusBar QLabel {{ + font-weight: bold; + padding: 2px 8px; +}} + +/* ─── Scroll Bars ───────────────────────────────────────────────── */ + +QScrollBar:vertical {{ + background-color: {COLORS['bg_dark']}; + width: 10px; + border: none; +}} + +QScrollBar::handle:vertical {{ + background-color: {COLORS['border_light']}; + border-radius: 5px; + min-height: 30px; +}} + +QScrollBar::handle:vertical:hover {{ + background-color: {COLORS['accent']}; +}} + +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ + height: 0px; +}} + +QScrollBar:horizontal {{ + background-color: {COLORS['bg_dark']}; + height: 10px; + border: none; +}} + +QScrollBar::handle:horizontal {{ + background-color: {COLORS['border_light']}; + border-radius: 5px; + min-width: 30px; +}} + +QScrollBar::handle:horizontal:hover {{ + background-color: {COLORS['accent']}; +}} + +QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ + width: 0px; +}} + +/* ─── Tooltips ──────────────────────────────────────────────────── */ + +QToolTip {{ + background-color: {COLORS['bg_medium']}; + color: {COLORS['text']}; + border: 1px solid {COLORS['accent']}; + border-radius: 4px; + padding: 6px; + font-size: 12px; +}} + +/* ─── Message Boxes ─────────────────────────────────────────────── */ + +QMessageBox {{ + background-color: {COLORS['bg_medium']}; +}} + +QMessageBox QLabel {{ + color: {COLORS['text']}; +}} + +QMessageBox QPushButton {{ + min-width: 80px; +}} + +/* ─── Splitter ──────────────────────────────────────────────────── */ + +QSplitter::handle {{ + background-color: {COLORS['border']}; + height: 3px; +}} + +QSplitter::handle:hover {{ + background-color: {COLORS['accent']}; +}} + +""" diff --git a/python/tests/test_rx_realtime.py b/python/tests/test_rx_realtime.py index 42bcbb5..1b62132 100644 --- a/python/tests/test_rx_realtime.py +++ b/python/tests/test_rx_realtime.py @@ -101,9 +101,9 @@ class TestChangeHighlighting: frame_item = window.rx_table.topLevelItem(0) temp_sig = frame_item.child(1) # MotorTemp changed: 0x10 → 0x20 bg = temp_sig.background(4) - # Should be yellow (255, 255, 100) - assert bg.color().red() == 255 - assert bg.color().green() == 255 + # Should be highlighted (amber/yellow with alpha) + assert bg.color().red() > 200 + assert bg.color().green() > 150 def test_unchanged_signal_not_highlighted(self, window): """Signal that didn't change should not be highlighted."""