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