You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

542 lines
21 KiB

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**vzug-e-hinge Test & Control System** - A PyQt6-based desktop application for testing and controlling e-hinge devices through UART and I2C interfaces. The system provides automated test session execution, real-time data logging, packet detection, and telemetry storage in SQLite.
## Running the Application
```bash
# Install dependencies
pip install -r requirements.txt
# Run the application (uses default database path: ./database/ehinge.db)
python main.py
# Run with custom database path
python main.py /path/to/custom.db
```
## Database Management
```bash
# Initialize/recreate database
python database/init_database.py
# Recreate database (overwrite existing)
python database/init_database.py --overwrite
# Check database health
python database/init_database.py --check
```
## Architecture
### Application Entry Point
- `main.py` - Main application window with tabbed interface (Session, Configure Session, Configure Interface, UART, I2C, Graph)
- `MainWindow` class manages all tabs and coordinates database connection
### Core Session Execution Flow
The application uses a **three-layer execution model**:
1. **Session Layer** (`session.py`) - Orchestrates the complete test session
- Loads interface and session profiles from database
- Opens/closes hardware ports (UART command, UART logger, I2C)
- Manages command sequence execution
- Handles pause/stop requests (queued after current run)
- Emits PyQt signals for real-time GUI updates
2. **Run Layer** (`run.py`) - Executes a single RUN (one command with data collection)
- Configures packet detection with callback
- Sends UART command via command port
- Collects telemetry packets from logger port (if enabled)
- Triggers I2C reads correlated to UART timestamps
- Decodes data and saves to database
3. **Hardware Layer** (`uart/uart_kit/uart_core.py`, `i2c/i2c_kit/i2c_core.py`) - Low-level interface management
- UART: Packet detection, circular buffer, reader threads, timestamping
- I2C: Bus operations, device scanning, continuous logging
### Critical Architectural Concepts
#### Dual UART Port System
The application uses **two separate UART ports** for different purposes:
- **UART Command Port** (TX/RX): Sends commands to device and receives ACK/responses
- **UART Logger Port** (RX only, optional): Receives telemetry packets from device
- This separation allows simultaneous command sending and telemetry logging
- Logger port can be disabled for simple command/response mode
#### Packet Detection vs. Raw Mode
- **Packet Detection Enabled**: UART Logger port detects packets with start/end markers, used for telemetry streams
- **Packet Detection Disabled**: Raw TX/RX mode for simple command/response (set `print_command_rx=True` in session profile)
#### Queued Pause/Stop
- Pause and Stop requests are **queued** and execute AFTER the current run completes
- Users can press pause/stop anytime, but it only takes effect during the delay phase between runs
- This prevents data corruption by ensuring runs complete atomically
#### PGKomm2 Protocol (V-ZUG Serial Communication)
The e-hinge devices use **PGKomm2** protocol (V-ZUG spec A5.5093D-AB) for UART communication:
**Frame Format:**
```
DD 22 | ADR1 ADR2 | LEN | DATA (0-255 bytes) | BCC
MAGIC | Address | Len | Payload | Checksum
```
**Key Characteristics:**
- **MAGIC**: `0xDD` (221), **INVMAGIC**: `0x22` (34)
- **Length-delimited**: LEN field specifies DATA length (no terminator bytes like `\n`)
- **Address swap**: Slave response swaps ADR1/ADR2 (if master sends `PH`, slave responds `HP`)
- **BCC**: Block check character calculated from ADR1 through DATA (XOR checksum) - **VALIDATED on receive**
- **Timing**: Response time < 15 ms, inter-byte time < 10 ms
- **Baud rates**: 4800 or 115200 baud, 8N1 with **Even Parity**
**Common Addresses:**
- `PH` (`0x50 0x48`): Command from master Echo from slave
- `HP` (`0x48 0x50`): Response from slave
- `SB` (`0x53 0x42`): Status broadcast
**Multiple Frames:**
Device typically sends multiple frames per command:
1. **Echo (PH)**: Device echoes received command
2. **Response (HP)**: Actual response with data
3. **Status (SB)**: Optional status update
Example:
```
TX: DD 22 50 48 02 43 4F 16 (Master sends PH command)
RX: DD 22 50 48 02 43 4F 16 (Echo: PH)
RX: DD 22 48 50 02 43 4F 16 (Response: HP, addresses swapped)
RX: DD 22 53 42 01 4E 5E (Status: SB)
```
**Documentation:**
- Protocol spec: `v_zug_documentation/PGKomm2_Spec_A5.5093D_AB.pdf`
- Application telegrams: `v_zug_documentation/PgKomm2_ Application Telegrams-v122-20230417_181422.pdf`
**Quick Start (Final Working Implementation):**
1. **GUI defaults** automatically set: Parity=Even, Mode=PGKomm2, Timeout=30ms
2. **Connect** waits 100ms settle delay
3. **First 1-2 commands may timeout** (device synchronization)
4. **After 3-4 commands** rock solid reliability
5. **BCC errors logged to console** if frame corruption detected
**Implementation Notes (`uart/uart_kit/uart_core.py::uart_send_and_read_pgkomm2()`):**
The implementation uses a **simple single-loop approach** (matches old working code exactly):
```python
# Flush buffer and send
reset_input_buffer()
uart_write(tx_data)
# Single loop with deadline
deadline = now + 30ms # Total timeout (spec <15ms + real-world margin)
while now < deadline:
if bytes_available:
rx_buffer += read_all_available() # Read IMMEDIATELY
# Extract frames using LEN field
while True:
frame = extract_frame(rx_buffer) # Uses LEN to know size
validate_BCC(frame) # Reject corrupted
if frame is None:
break # Need more bytes
collected_frames.append(frame)
if frame.address == HP:
return collected_frames # Stop on HP!
# No sleep - just spin
```
**Key Implementation Details:**
- **Single timeout (30ms)**: Total window for entire operation (spec says < 15ms, +15ms margin for real-world conditions)
- **Read immediately**: When bytes arrive, read them instantly (no delays)
- **No sleep**: Pure spinning for minimum latency
- **Stop on HP**: Exits immediately when HP response detected
- **BCC validation**: Every frame validated before acceptance - corrupted frames rejected and logged
- **LEN field**: Uses frame length to know exactly how many bytes to read
- **Simple**: No phases, no grace periods, just read-parse-detect loop
- **Debug logging**: TIMEOUT and IO_ERROR cases logged with buffer contents for diagnostics
**Default GUI Settings (for PGKomm2):**
- **Parity**: Even (required by spec)
- **Mode**: PGKomm2
- **Timeout**: 30ms (hidden, automatic)
**Why 30ms (not 15ms)?**
- **Spec says < 15ms**: Ideal conditions only
- **Real world**: Background SB telemetry, device load, USB latency, reader thread stop/restart add overhead
- **Testing results**: 20ms still showed occasional timeouts, 30ms more reliable
- **Still fast**: Stops immediately on HP detection (typically 8-12ms actual response time)
**Error Handling:**
- **TIMEOUT**: No data received within 30ms (device not responding) or got frames but no HP response
- **IO_ERROR**: Unparseable data in buffer (incomplete frame or BCC failure)
- **BCC ERROR**: Logged to console when checksum validation fails (electrical noise/corruption)
- Format: `[PGKOMM2] ✗ BCC FAIL: ADR=XX YY, calc=ZZ, recv=WW`
- Shows calculated vs received BCC and full frame hex dump
**Connection Behavior:**
- **100ms settle delay**: After port opens, waits 100ms for device to stabilize
- **Reason**: Device may reset on DTR/RTS change, needs boot/initialization time
- **First commands**: May still timeout if device is sending SB status broadcasts
- **After 3-4 commands**: Device synchronized, all commands work reliably
**Threading Architecture:**
- **UART Reader Thread**: Always running (except during PGKomm2 operations)
- Reads serial port circular buffer continuously
- Most modes read from circular buffer
- **PGKomm2**: No worker thread - called directly from GUI thread
- Blocks GUI for ~30ms (acceptable, keeps threading simple)
- **Temporarily stops reader thread** for exclusive serial access (prevents race condition)
- Reads directly from serial port (not circular buffer - lower latency)
- Restarts reader thread after completion
- **Why stop reader thread?**: PGKomm2 needs direct serial reads for low latency. Stopping reader prevents both threads competing for same bytes.
**Key Design Decisions (Lessons Learned):**
1. **Simplicity Over Complexity**
- Tried: Worker threads, two-phase reading, packet-driven with circular buffer
- Final: Direct serial reads with reader thread temporarily stopped
- Lesson: Simple approach that matches old working code is best
2. **Timeout Tuning**
- Spec: < 15ms response time
- Reality: 30ms needed (background telemetry, thread overhead, USB latency)
- Testing: 15ms occasional timeouts, 20ms still issues, 30ms reliable
3. **Thread Safety**
- Problem: Reader thread and PGKomm2 competing for serial bytes (race condition)
- Solution: Stop reader during PGKomm2 operations (exclusive access)
- Benefit: Simple, predictable, no race conditions
4. **BCC Validation Critical**
- Initially: Not validated (trusted device)
- Reality: Occasional electrical noise/corruption
- Solution: Validate BCC, log errors, reject corrupted frames
- Result: More robust, easier debugging
5. **First Commands After Connect**
- Device needs time to stabilize after DTR/RTS change
- 100ms settle delay helps but not sufficient
- Accept first 1-2 timeouts as normal device synchronization
- After 3-4 commands: rock solid
**PGKomm2 Implementation Status: ✅ Production Ready**
### Database Schema
Located in `database/init_database.py`:
**Profiles:**
- `interface_profiles` - Hardware configuration (UART command/logger ports, I2C bus, packet detection settings)
- `session_profiles` - Test sequences (JSON command list with delays and repetitions)
- `gui_profiles` - GUI appearance settings
**Execution:**
- `sessions` - Session records (links to profiles, tracks total runs, status)
- `uart_commands` - Predefined UART commands (hex strings, categories)
- `i2c_commands` - Predefined I2C operations (register access)
**Telemetry:**
- `telemetry_raw` - Raw backup data (UART packets and I2C bytes as BLOB)
- `telemetry_decoded` - Processed data (motor current, encoder values, angles, etc.)
Both telemetry tables use nanosecond timestamps (`t_ns`) for precise correlation between UART and I2C data.
### Widget Organization
**Configuration Widgets:**
- `configure_interface_widget.py` - Create/edit interface profiles (UART/I2C settings)
- `configure_session_widget.py` - Create/edit session profiles (command sequences)
**Control Widgets:**
- `session_widget.py` - Main control panel for running sessions (start/pause/stop, profile selection, status display)
- `uart/uart_integrated_widget.py` - Manual UART testing (command table + direct control)
- `i2c/i2c_integrated_widget.py` - Manual I2C testing (command table + direct control)
**UART Widget Modes (`uart/uart_kit/uart_core_widget.py`):**
The UART widget supports 4 operational modes:
1. **Request-Response**: Send command, wait for response packet (timeout or terminator-based)
- Uses packet detection with configurable stop condition
- Best for generic packet-based protocols
2. **Polling**: Continuous packet detection with grace period
- Waits for first byte (grace period), then captures burst
- Supports packet detection with callbacks
- Best for streaming telemetry
3. **Listening**: Raw stream mode with no stop condition
- Captures everything continuously
- Manual buffer reading required
- Best for debugging and raw data capture
4. **PGKomm2**: V-ZUG protocol with DD 22 framing and automatic HP detection
- **Automatic response detection** - No timing configuration needed
- Stops immediately when HP response detected (typically 5-10ms)
- 20ms safety timeout (hardcoded, hidden from user)
- Automatically parses multiple frames (PH echo + HP response)
- Identifies frame types by address bytes (PH/HP/SB)
- **Requires EVEN parity** (115200 8E1 per spec)
- **Use this mode for e-hinge device communication**
**Session Worker:**
- `session_worker.py` - QThread wrapper for running sessions in background without blocking GUI
### Supporting Modules
- `decoder.py` - Packet decoding logic (UART telemetry and I2C angle conversion)
- `global_clock.py` - Shared timestamp source for UART/I2C synchronization
- `graph_table_query.py` - Database query utilities for graphing
- `buffer_kit/circular_buffer.py` - Thread-safe circular buffer for data streams
- `command_table/command_table.py` - Reusable command table widget
## Development Workflow
### Adding New UART Commands
1. Insert into `uart_commands` table via database or GUI
2. Commands are immediately available in session profiles and manual testing
3. Format: `hex_string` as space-separated hex bytes (e.g., "DD 22 50 48 02 41 52 09")
### Creating Test Profiles
1. Create interface profile with port settings and packet detection config
2. Create session profile with JSON command sequence:
```json
{
"commands": [
{
"command_id": 1,
"delay_ms": 3000
},
{
"command_id": 5,
"delay_ms": 5000
}
]
}
```
3. Use session widget to execute profile
### Database Size Management
- Max size: 2 GB (configured in `init_database.py`)
- Monitor via status bar (shows percentage and color-coded warning)
- Use "Database > Vacuum Database" to reclaim space after deletions
## Port Configuration Notes
**UART Ports:**
- Linux: `/dev/ttyUSB0`, `/dev/ttyACM0`, etc.
- Must have read/write permissions (add user to `dialout` group)
- Typical baud rates: 9600, 115200
**I2C Bus:**
- Linux: Bus ID corresponds to `/dev/i2c-X` (e.g., bus_id=1 means `/dev/i2c-1`)
- Requires `smbus2` library
- Must have read/write permissions (add user to `i2c` group)
## Signal/Slot Architecture
The Session class emits PyQt signals for GUI updates:
- `session_started(session_id)` - Session begins
- `command_started(command_no, command_name)` - Command starts
- `run_completed(run_no, packet_count)` - Run finishes
- `delay_countdown(seconds_remaining)` - Countdown during delays
- `session_paused()` - Session paused
- `session_finished()` - Session completes
- `error_occurred(error_message)` - Error encountered
- `status_changed(status_text)` - General status update
- `raw_data_received(direction, hex_string)` - TX/RX data for display
Connect to these signals in widgets for real-time updates without polling.
## Testing
Individual modules can be tested directly:
```bash
# Test UART core
python uart/uart_kit/uart_core.py
# Test I2C core
python i2c/i2c_kit/i2c_core.py
# Test circular buffer
python buffer_kit/circular_buffer_test.py
```
## Code Style Notes
- Type hints used throughout (Python 3.10+)
- Dataclasses for configuration structures
- Enums for status codes and modes
- Thread-safe operations with locks where needed
- Extensive docstrings with Args/Returns/Raises sections
---
## Technical Challenges & Solutions
### Challenge 1: GUI Threading & Responsiveness
**Problem:** Qt requires GUI to never block. Session execution is a long-running loop that would freeze the interface.
**Solution:** `session_worker.py` - QThread wrapper that:
- Runs session in background thread
- Creates thread-local SQLite connection (SQLite objects cannot cross threads)
- Uses Qt signals for thread-safe GUI updates
- Swaps database connections before/after execution
**Critical Code:**
```python
# session_worker.py
thread_db_conn = sqlite3.connect(self.db_path) # Thread-local
self.session.db_conn = thread_db_conn # Temporary swap
self.session.start_session() # Blocking call runs in background
```
### Challenge 2: Timestamp Correlation (UART ↔ I2C)
**Problem:** UART packets arrive at unpredictable times. I2C angle readings must be timestamped at the exact moment the UART packet is detected to correlate motor state with door position.
**Solution:** Callback-based triggering in `run.py`:
```python
def on_uart_packet_detected(timestamp_ns: int):
"""Called IMMEDIATELY when packet detected"""
if i2c_port:
status, i2c_bytes = i2c_read_block(i2c_port, ...)
store_with_timestamp(timestamp_ns, i2c_bytes) # Same timestamp!
```
**Flow:**
1. UART reader thread detects packet start → captures `timestamp_ns`
2. Calls callback immediately with timestamp
3. Callback reads I2C angle sensor
4. Both readings share identical timestamp → perfect correlation in database
**Hardware Notes:**
- Two physically separate USB UART interfaces (command + logger)
- I2C read must complete before next UART packet arrives
- Blocking I2C read is acceptable if fast enough (<17 byte receive time @ baud rate)
### Challenge 3: Circular Buffer Absolute Positioning
**Problem:** Standard circular buffers wrap around. Need to extract data spans that might cross the wrap boundary using absolute offsets.
**Solution:** `buffer_kit/circular_buffer.py` tracks `write_absolute` (total bytes ever written):
```python
write_absolute: int # Monotonic counter, never wraps
position_in_buffer = write_absolute % capacity # Actual index
# Extract span handles wrapping automatically
cb_copy_span(buffer, start_abs=1000, end_abs=1050) # Works even if wrapped
```
**Use Case:** Read UART/I2C ring buffers after run completes, even if data wrapped during acquisition.
### Challenge 4: Grace Period vs Stop Timeout
**Problem:** Device may take time to respond. Need to distinguish:
- **Grace period**: Wait for FIRST byte after command sent
- **Stop timeout**: Silence between packets within a burst
**Solution in UART config:**
```python
polling_mode=True # Enable grace period
grace_timeout_ms=150 # Wait up to 150ms for first byte
stop_timeout_ms=150 # Silence between packets
```
**Behavior:**
1. Send command
2. Wait up to `grace_timeout_ms` for first byte If no byte: "Device not responding"
3. Once first byte arrives, switch to stop condition mode
4. If `stop_timeout_ms` passes with no data Packet burst complete
**Configured per interface profile** in `interface_profiles` table.
### Challenge 5: Buffer Sizing
**Configuration:** 40MB buffers for both UART and I2C
**Rationale:**
- Buffers cleared per run, not shared across runs
- Large buffers prevent overflow during long runs
- System has 8GB RAM + 32GB NVMe 40MB per port is negligible
- Prevents data loss if device sends unexpected burst
**Locations:**
- Default: `uart_core.py` line 139, `i2c_core.py` line 72
- Session overrides: `session.py` _open_ports() method
### Challenge 6: Error Reporting
**Requirement:** Track I2C read failures without failing the entire run.
**Solution in `run.py`:**
```python
self.i2c_failures = 0 # Counter initialized
# In callback:
if i2c_read_status != OK:
self.i2c_failures += 1 # Count but continue
# At end of run:
if self.i2c_failures > 0:
raw_data_callback("ERROR", f"I2C failures: {self.i2c_failures}")
```
**Display:** Errors appear in GUI Data Monitor, not as run failure.
### Challenge 7: Queued Pause/Stop
**Problem:** User presses Stop during run execution. Cannot stop immediately (would corrupt data, leave hardware in bad state).
**Solution:** Flag-based queuing:
```python
self.stop_queued = False # Flag set by user action
# In command loop:
for cmd in commands:
if self.stop_queued: # Check BEFORE starting run
finalize_and_return()
execute_run(...) # Runs atomically
if self.stop_queued: # Check during delay
finalize_and_return()
```
**Behavior:** Pause/Stop take effect between runs, not during. Current run always completes.
## Configuration Details
### Stop Timeout Loading
**Fixed:** Stop timeout and grace period are now loaded from `interface_profiles` table and passed to `execute_run()`.
**Previous Issue:** Hardcoded 5000ms timeout Now uses configured value (typically 150ms)
**Files Modified:**
- `session.py` line 190: Added `uart_logger_grace_ms` to SQL query
- `session.py` line 222: Added to `interface_config` dict
- `session.py` line 742: Changed from hardcoded 5000 to `interface_config['uart_logger_timeout_ms']`
- `session.py` lines 341-349, 378-388: Full UART config with all parameters
- `session.py` line 476: I2C config with 40MB buffer
### Typical Configuration Values
```python
uart_logger_timeout_ms = 150 # Packet silence timeout
uart_logger_grace_ms = 150 # Wait for first byte
uart_command_timeout_ms = 1000 # Command response timeout
buffer_size = 40 * 1024 * 1024 # 40MB per port
```
## Diagrams
Block diagrams available in `diagram/` folder:
- `Block Diagram.xml` - Overall system architecture (3 layers)
- `run.xml` - Single RUN execution flow
- `session.xml` - Session configuration and command loop
View with draw.io or compatible tool.