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) <noreply@anthropic.com>
This commit is contained in:
Mohamed Salem 2026-04-08 03:28:02 +02:00
parent 251f5d327e
commit c6f0d2fdde
6 changed files with 686 additions and 19 deletions

View File

@ -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:**

186
README.md Normal file
View File

@ -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.

View File

@ -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()

View File

@ -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 ──────────────────────────────────────────────────

476
python/src/theme.py Normal file
View File

@ -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']};
}}
"""

View File

@ -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."""