parent
0277eec76a
commit
ac173bf6f3
|
After Width: | Height: | Size: 104 KiB |
@ -0,0 +1,541 @@
|
|||||||
|
# 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.
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,15 +0,0 @@
|
|||||||
contourpy==1.3.3
|
|
||||||
cycler==0.12.1
|
|
||||||
fonttools==4.60.1
|
|
||||||
kiwisolver==1.4.9
|
|
||||||
matplotlib==3.10.7
|
|
||||||
numpy==2.3.4
|
|
||||||
packaging==25.0
|
|
||||||
pillow==12.0.0
|
|
||||||
pyparsing==3.2.5
|
|
||||||
PyQt6==6.10.0
|
|
||||||
PyQt6-Qt6==6.10.0
|
|
||||||
PyQt6_sip==13.10.2
|
|
||||||
pyserial==3.5
|
|
||||||
python-dateutil==2.9.0.post0
|
|
||||||
six==1.17.0
|
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
# -----------------------
|
||||||
|
# File: components/uart/packet_detector.py
|
||||||
|
#
|
||||||
|
# Purpose
|
||||||
|
# -------
|
||||||
|
# Tiny, fast packet sniffer for fixed-format frames in a byte stream.
|
||||||
|
# Detects packets *in real time* as bytes arrive, without conversions.
|
||||||
|
#
|
||||||
|
# Default packet shape
|
||||||
|
# --------------------
|
||||||
|
# - Start: 0xEF 0xFE
|
||||||
|
# - Total length: 14 bytes
|
||||||
|
# - Last byte must be: 0xEE
|
||||||
|
#
|
||||||
|
# When a packet is detected, we invoke an optional callback:
|
||||||
|
# on_packet(ts_ns: int, packet: bytes, abs_off_start: int) -> None
|
||||||
|
# where `ts_ns` is the detection/completion timestamp (now_ns at the moment
|
||||||
|
# we saw the last byte), and `abs_off_start` is the absolute offset of the
|
||||||
|
# first packet byte within the upstream ring buffer (if provided by caller).
|
||||||
|
#
|
||||||
|
# Notes
|
||||||
|
# -----
|
||||||
|
# - The detector keeps a tiny internal buffer (max 14 bytes) and a simple FSM.
|
||||||
|
# - It is transport-agnostic: you feed() bytes from any source (UART, I2C, ...).
|
||||||
|
# - "Absolute offset" is optional. If you pass a base offset for each chunk,
|
||||||
|
# the detector computes the packet's start offset precisely.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional, Callable
|
||||||
|
|
||||||
|
|
||||||
|
class PacketDetector:
|
||||||
|
"""Realtime detector for fixed 14-byte packets starting with EF FE and ending with EE.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
det = PacketDetector(on_packet=my_handler)
|
||||||
|
det.feed(data, t_ns, abs_off_start)
|
||||||
|
|
||||||
|
Where `abs_off_start` is the absolute ring offset of `data[0]`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"_on_packet",
|
||||||
|
"_buf",
|
||||||
|
"_count",
|
||||||
|
"_searching",
|
||||||
|
"_have_ef",
|
||||||
|
"_pkt_len",
|
||||||
|
"_start0",
|
||||||
|
"_start1",
|
||||||
|
"_end_byte",
|
||||||
|
"_pending_first_abs_off",
|
||||||
|
"_pending_first_ts_ns",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
on_packet: Optional[Callable[[int, bytes, int], None]] = None,
|
||||||
|
*,
|
||||||
|
start0: int = 0xEF,
|
||||||
|
start1: int = 0xFE,
|
||||||
|
end_byte: int = 0xEE,
|
||||||
|
length: int = 14,
|
||||||
|
) -> None:
|
||||||
|
self._on_packet = on_packet
|
||||||
|
self._buf = bytearray()
|
||||||
|
self._count = 0
|
||||||
|
self._searching = True
|
||||||
|
self._have_ef = False # helper to detect EF FE efficiently
|
||||||
|
self._pkt_len = int(length)
|
||||||
|
self._start0 = int(start0) & 0xFF
|
||||||
|
self._start1 = int(start1) & 0xFF
|
||||||
|
self._end_byte = int(end_byte) & 0xFF
|
||||||
|
self._pending_first_abs_off = 0
|
||||||
|
self._pending_first_ts_ns = 0
|
||||||
|
|
||||||
|
# ---------------------------
|
||||||
|
# Configuration
|
||||||
|
# ---------------------------
|
||||||
|
def set_handler(self, fn: Optional[Callable[[int, bytes, int], None]]):
|
||||||
|
"""Register or clear the on_packet callback."""
|
||||||
|
self._on_packet = fn
|
||||||
|
|
||||||
|
# ---------------------------
|
||||||
|
# Core detection
|
||||||
|
# ---------------------------
|
||||||
|
def feed(self, data: bytes, t_ns: int, abs_off_start: int = 0) -> None:
|
||||||
|
"""Consume a chunk of bytes and emit packets via callback when found.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data : bytes
|
||||||
|
Newly received bytes.
|
||||||
|
t_ns : int
|
||||||
|
Timestamp for *this chunk* (e.g., when the first byte was read).
|
||||||
|
We use this for the first-byte time if desired; for simplicity the
|
||||||
|
emitted event uses the completion time (call site passes a fresh now).
|
||||||
|
abs_off_start : int
|
||||||
|
Absolute offset of data[0] within the upstream ring buffer. Used to
|
||||||
|
compute the absolute start offset of detected packets.
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fast path: work byte-by-byte, minimal branching
|
||||||
|
for i, b in enumerate(data):
|
||||||
|
bi = b & 0xFF
|
||||||
|
|
||||||
|
if self._searching:
|
||||||
|
# Find EF FE start sequence without growing the buffer
|
||||||
|
if not self._have_ef:
|
||||||
|
self._have_ef = bi == self._start0
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if bi == self._start1:
|
||||||
|
# Start found: reset buffer to EF FE
|
||||||
|
self._buf.clear()
|
||||||
|
self._buf.append(self._start0)
|
||||||
|
self._buf.append(self._start1)
|
||||||
|
self._count = 2
|
||||||
|
self._searching = False
|
||||||
|
self._pending_first_abs_off = (
|
||||||
|
abs_off_start + i - 1
|
||||||
|
) # index of EF
|
||||||
|
self._pending_first_ts_ns = t_ns
|
||||||
|
# Regardless, reset EF tracker (we only accept EF FE)
|
||||||
|
self._have_ef = bi == self._start0
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Accumulating a candidate packet
|
||||||
|
self._buf.append(bi)
|
||||||
|
self._count += 1
|
||||||
|
|
||||||
|
if self._count < self._pkt_len:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# We have exactly `length` bytes; check end marker
|
||||||
|
if self._buf[-1] == self._end_byte:
|
||||||
|
# Detected a full packet → emit
|
||||||
|
if self._on_packet is not None:
|
||||||
|
# Completion timestamp should be the call-site's fresh now.
|
||||||
|
# We re-use t_ns here; callers should pass a fresh now when feeding.
|
||||||
|
self._on_packet(t_ns, bytes(self._buf), self._pending_first_abs_off)
|
||||||
|
# Reset to search for the next packet, even if the tail byte was wrong
|
||||||
|
self._buf.clear()
|
||||||
|
self._count = 0
|
||||||
|
self._searching = True
|
||||||
|
self._have_ef = False
|
||||||
|
self._pending_first_abs_off = 0
|
||||||
|
self._pending_first_ts_ns = 0
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Clear detector state (used on reconnects, etc.)."""
|
||||||
|
self._buf.clear()
|
||||||
|
self._count = 0
|
||||||
|
self._searching = True
|
||||||
|
self._have_ef = False
|
||||||
|
self._pending_first_abs_off = 0
|
||||||
|
self._pending_first_ts_ns = 0
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
# File: components/uart/uart_command_editor.py
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QDialog,
|
||||||
|
QVBoxLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLineEdit,
|
||||||
|
QPushButton,
|
||||||
|
QFormLayout,
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
|
||||||
|
class UartCommandEditorDialog(QDialog):
|
||||||
|
def __init__(self, command=None):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.setWindowTitle("Modify UART Command" if command else "Add UART Command")
|
||||||
|
|
||||||
|
# === Create form layout for consistent label alignment ===
|
||||||
|
form_layout = QFormLayout()
|
||||||
|
form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
|
||||||
|
form_layout.setFormAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
|
form_layout.setSpacing(10)
|
||||||
|
|
||||||
|
# === Input Fields ===
|
||||||
|
self.name_input = QLineEdit()
|
||||||
|
self.category_input = QLineEdit()
|
||||||
|
self.description_input = QLineEdit()
|
||||||
|
self.hex_input = QLineEdit()
|
||||||
|
|
||||||
|
# === Prefill if modifying ===
|
||||||
|
if command:
|
||||||
|
self.name_input.setText(command.get("name", ""))
|
||||||
|
self.category_input.setText(command.get("category", ""))
|
||||||
|
self.description_input.setText(command.get("description", ""))
|
||||||
|
self.hex_input.setText(
|
||||||
|
command.get("hex_string", command.get("hex_string", ""))
|
||||||
|
)
|
||||||
|
|
||||||
|
form_layout.addRow("Name:", self.name_input)
|
||||||
|
form_layout.addRow("Category:", self.category_input)
|
||||||
|
form_layout.addRow("Description:", self.description_input)
|
||||||
|
form_layout.addRow("Hex:", self.hex_input)
|
||||||
|
|
||||||
|
# === Buttons ===
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
self.ok_button = QPushButton("OK")
|
||||||
|
self.cancel_button = QPushButton("Cancel")
|
||||||
|
self.ok_button.clicked.connect(self.accept)
|
||||||
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
|
button_layout.addStretch()
|
||||||
|
button_layout.addWidget(self.ok_button)
|
||||||
|
button_layout.addWidget(self.cancel_button)
|
||||||
|
|
||||||
|
# === Final layout ===
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.addLayout(form_layout)
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
self.setMinimumWidth(400)
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return {
|
||||||
|
"name": self.name_input.text().strip(),
|
||||||
|
"category": self.category_input.text().strip(),
|
||||||
|
"hex_string": self.hex_input.text().strip(),
|
||||||
|
"description": self.description_input.text().strip(),
|
||||||
|
}
|
||||||
@ -0,0 +1,289 @@
|
|||||||
|
# components/uart/uart_ui.py
|
||||||
|
# One-file UART page: dedicated handler + UI (no base_handler, no protocol_ui).
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget,
|
||||||
|
QVBoxLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QPushButton,
|
||||||
|
QComboBox,
|
||||||
|
QLineEdit,
|
||||||
|
QSplitter,
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
# project-local deps (unchanged)
|
||||||
|
from components.console.console_ui import console_widget
|
||||||
|
from components.console.console_registry import log_main_console
|
||||||
|
import components.items.elements as elements
|
||||||
|
from components.commands.command_table_ui import command_table_widget
|
||||||
|
import config.config as config
|
||||||
|
|
||||||
|
# UART logic + dialogs + db (unchanged)
|
||||||
|
from components.uart.uart_logic import UART_logic
|
||||||
|
from components.uart.uart_command_editor import UartCommandEditorDialog
|
||||||
|
from components.data import db
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Internal, dedicated UART handler
|
||||||
|
# -----------------------------
|
||||||
|
# -----------------------------
|
||||||
|
# Thin handler that calls back into the UI widget
|
||||||
|
# -----------------------------
|
||||||
|
class UartHandler:
|
||||||
|
"""
|
||||||
|
Used by command_table_widget. It doesn't own UART logic.
|
||||||
|
It just forwards actions to the parent UartWidget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, widget: "UartWidget"):
|
||||||
|
self.w = widget # back-reference to the UI
|
||||||
|
|
||||||
|
# table may request the list via the handler
|
||||||
|
def get_command_list(self):
|
||||||
|
return self.w.uart_logic.get_predefined_commands()
|
||||||
|
|
||||||
|
# ----- actions triggered from the command table -----
|
||||||
|
def send_command(self, command: dict):
|
||||||
|
# command has "hex_string"
|
||||||
|
self.w.send_command(command)
|
||||||
|
|
||||||
|
# command editor ops (invoked from command table)
|
||||||
|
def add_command(self):
|
||||||
|
dialog = UartCommandEditorDialog()
|
||||||
|
if dialog.exec():
|
||||||
|
command = dialog.get_data()
|
||||||
|
db.add_uart_command(command)
|
||||||
|
|
||||||
|
def modify_command(self, command):
|
||||||
|
dialog = UartCommandEditorDialog(command=command)
|
||||||
|
if dialog.exec():
|
||||||
|
updated_command = dialog.get_data()
|
||||||
|
updated_command["id"] = command["id"]
|
||||||
|
db.modify_uart_command(updated_command)
|
||||||
|
|
||||||
|
def delete_command(self, command):
|
||||||
|
db.delete_uart_command(command)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# UART UI (layout preserved)
|
||||||
|
# -----------------------------
|
||||||
|
class UartWidget(QWidget):
|
||||||
|
"""
|
||||||
|
Drops in where your old ProtocolUIWidget(UART) was used.
|
||||||
|
Same controls, same enable/disable behavior, same splitter/table/console layout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.uart_logic = UART_logic() # all comms live here
|
||||||
|
self.handler = UartHandler(self) # thin handler for table actions
|
||||||
|
self.commands = self.uart_logic.get_predefined_commands()
|
||||||
|
self.comboboxes = {}
|
||||||
|
self.connection_status = False
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
# === Top Control Row ===
|
||||||
|
top_controls = QWidget()
|
||||||
|
top_controls_layout = QHBoxLayout(top_controls)
|
||||||
|
top_controls_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
top_controls_layout.setSpacing(12)
|
||||||
|
top_controls_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
|
|
||||||
|
# Dynamically create comboboxes based on handler configuration
|
||||||
|
# Static definition instead of get_combobox_config()
|
||||||
|
self.comboboxes["port"] = QComboBox()
|
||||||
|
self.comboboxes["port"].addItems(self.uart_logic.get_channels())
|
||||||
|
top_controls_layout.addWidget(
|
||||||
|
elements.label_and_widget("Port", self.comboboxes["port"])
|
||||||
|
)
|
||||||
|
# Refresh button
|
||||||
|
self.button_refresh = elements.create_icon_button(
|
||||||
|
config.REFRESH_BUTTON_ICON_LINK, icon_size=30, border_size=4
|
||||||
|
)
|
||||||
|
top_controls_layout.addWidget(self.button_refresh)
|
||||||
|
|
||||||
|
self.comboboxes["baudrate"] = QComboBox()
|
||||||
|
self.comboboxes["baudrate"].addItems(self.uart_logic.get_baud_rates())
|
||||||
|
top_controls_layout.addWidget(
|
||||||
|
elements.label_and_widget("Baudrate", self.comboboxes["baudrate"])
|
||||||
|
)
|
||||||
|
|
||||||
|
self.comboboxes["data_bits"] = QComboBox()
|
||||||
|
self.comboboxes["data_bits"].addItems(self.uart_logic.get_data_bits())
|
||||||
|
top_controls_layout.addWidget(
|
||||||
|
elements.label_and_widget("Data Bits", self.comboboxes["data_bits"])
|
||||||
|
)
|
||||||
|
|
||||||
|
self.comboboxes["stop_bits"] = QComboBox()
|
||||||
|
self.comboboxes["stop_bits"].addItems(self.uart_logic.get_stop_bits())
|
||||||
|
top_controls_layout.addWidget(
|
||||||
|
elements.label_and_widget("Stop Bits", self.comboboxes["stop_bits"])
|
||||||
|
)
|
||||||
|
|
||||||
|
self.comboboxes["parity"] = QComboBox()
|
||||||
|
self.comboboxes["parity"].addItems(self.uart_logic.get_parity())
|
||||||
|
top_controls_layout.addWidget(
|
||||||
|
elements.label_and_widget("Parity", self.comboboxes["parity"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Connect / Disconnect
|
||||||
|
self.button_connect = QPushButton("Connect")
|
||||||
|
top_controls_layout.addWidget(
|
||||||
|
elements.label_and_widget("", self.button_connect)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.button_disconnect = QPushButton("Disconnect")
|
||||||
|
top_controls_layout.addWidget(
|
||||||
|
elements.label_and_widget("", self.button_disconnect)
|
||||||
|
)
|
||||||
|
|
||||||
|
# === Command Table ===
|
||||||
|
self.command_table = command_table_widget(
|
||||||
|
commands=self.commands,
|
||||||
|
handler=self.handler, # table uses handler methods for send/CRUD
|
||||||
|
)
|
||||||
|
|
||||||
|
col1_widget = QWidget()
|
||||||
|
col1_layout = QVBoxLayout(col1_widget)
|
||||||
|
col1_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
col1_layout.setSpacing(4)
|
||||||
|
col1_layout.addWidget(self.command_table)
|
||||||
|
|
||||||
|
# === Input Field + Send Button ===
|
||||||
|
self.input_hex = QLineEdit()
|
||||||
|
self.button_send_raw = QPushButton("Send Raw")
|
||||||
|
|
||||||
|
input_line_layout = QHBoxLayout()
|
||||||
|
input_line_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
input_line_layout.setSpacing(4)
|
||||||
|
input_line_layout.addWidget(self.input_hex)
|
||||||
|
input_line_layout.addWidget(self.button_send_raw)
|
||||||
|
|
||||||
|
# === Console ===
|
||||||
|
self.console = console_widget()
|
||||||
|
self.uart_logic.set_logger(self.console.log)
|
||||||
|
|
||||||
|
console_stack_widget = QWidget()
|
||||||
|
console_stack_layout = QVBoxLayout(console_stack_widget)
|
||||||
|
console_stack_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
console_stack_layout.setSpacing(4)
|
||||||
|
console_stack_layout.addLayout(input_line_layout)
|
||||||
|
console_stack_layout.addWidget(self.console)
|
||||||
|
|
||||||
|
# === Horizontal Splitter: Table | Console (sizes preserved) ===
|
||||||
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||||
|
splitter.addWidget(col1_widget)
|
||||||
|
splitter.addWidget(console_stack_widget)
|
||||||
|
splitter.setSizes([740, 1200])
|
||||||
|
splitter.setStretchFactor(0, 0)
|
||||||
|
splitter.setStretchFactor(1, 1)
|
||||||
|
|
||||||
|
# === Main Layout ===
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
main_layout.setContentsMargins(4, 4, 4, 4)
|
||||||
|
main_layout.setSpacing(6)
|
||||||
|
main_layout.addWidget(top_controls)
|
||||||
|
main_layout.addWidget(splitter, stretch=1)
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
# === Signals ===
|
||||||
|
self.button_refresh.clicked.connect(self.refresh)
|
||||||
|
self.button_connect.clicked.connect(self.connect)
|
||||||
|
self.button_disconnect.clicked.connect(self.disconnect)
|
||||||
|
self.button_send_raw.clicked.connect(self.send_command_raw)
|
||||||
|
|
||||||
|
self.disconnected_enable_status()
|
||||||
|
|
||||||
|
# ---- UI state toggles (unchanged) ----
|
||||||
|
def disconnected_enable_status(self):
|
||||||
|
for combo in self.comboboxes.values():
|
||||||
|
elements.set_enabled_state(True, combo, grayOut=False)
|
||||||
|
|
||||||
|
elements.set_enabled_state(True, self.command_table, grayOut=False)
|
||||||
|
elements.set_enabled_state(False, self.input_hex, grayOut=True)
|
||||||
|
elements.set_enabled_state(False, self.button_send_raw, grayOut=True)
|
||||||
|
elements.set_enabled_state(False, self.button_disconnect, grayOut=True)
|
||||||
|
elements.set_enabled_state(True, self.button_connect, grayOut=False)
|
||||||
|
|
||||||
|
def connected_enable_status(self):
|
||||||
|
for combo in self.comboboxes.values():
|
||||||
|
elements.set_enabled_state(False, combo, grayOut=True)
|
||||||
|
|
||||||
|
elements.set_enabled_state(True, self.command_table, grayOut=False)
|
||||||
|
elements.set_enabled_state(True, self.input_hex, grayOut=False)
|
||||||
|
elements.set_enabled_state(True, self.button_send_raw, grayOut=False)
|
||||||
|
elements.set_enabled_state(True, self.button_disconnect, grayOut=False)
|
||||||
|
elements.set_enabled_state(False, self.button_connect, grayOut=True)
|
||||||
|
|
||||||
|
# ---- Button handlers (unchanged behavior) ----
|
||||||
|
def connect(self):
|
||||||
|
log_main_console("info", "🔗 Connecting...")
|
||||||
|
port = self.comboboxes["port"].currentText()
|
||||||
|
baudrate = int(self.comboboxes["baudrate"].currentText())
|
||||||
|
data_bits = int(self.comboboxes["data_bits"].currentText())
|
||||||
|
stop_bits = float(self.comboboxes["stop_bits"].currentText())
|
||||||
|
parity = self.comboboxes["parity"].currentText()[0].upper()
|
||||||
|
success = self.uart_logic.connect(
|
||||||
|
port=port,
|
||||||
|
baudrate=baudrate,
|
||||||
|
data_bits=data_bits,
|
||||||
|
stop_bits=stop_bits,
|
||||||
|
parity=parity,
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
self.connected_enable_status()
|
||||||
|
self.command_table.set_connected_state()
|
||||||
|
self.connection_status = True
|
||||||
|
else:
|
||||||
|
elements.flash_button(
|
||||||
|
self.button_connect, flash_style="background-color: red;"
|
||||||
|
)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
log_main_console("info", "🔌 Disconnecting...")
|
||||||
|
success = self.uart_logic.disconnect()
|
||||||
|
if success:
|
||||||
|
log_main_console("info", "🔌 Disconnecting...")
|
||||||
|
self.disconnected_enable_status()
|
||||||
|
self.command_table.set_disconnected_state()
|
||||||
|
self.connection_status = False
|
||||||
|
self.refresh(silent=True)
|
||||||
|
else:
|
||||||
|
elements.flash_button(
|
||||||
|
self.button_disconnect, flash_style="background-color: red;"
|
||||||
|
)
|
||||||
|
|
||||||
|
def refresh(self, silent: bool = False):
|
||||||
|
log_main_console("info", "🔄 Refreshing...")
|
||||||
|
self.comboboxes["port"].clear()
|
||||||
|
port = self.uart_logic.get_channels()
|
||||||
|
if port:
|
||||||
|
self.comboboxes["port"].addItems(port)
|
||||||
|
if not silent:
|
||||||
|
elements.flash_button(self.button_refresh)
|
||||||
|
log_main_console("success", "🔄 Refresehd")
|
||||||
|
else:
|
||||||
|
elements.flash_button(
|
||||||
|
self.button_refresh, flash_style="background-color: red;"
|
||||||
|
)
|
||||||
|
log_main_console("error", "🔄 Refresehd")
|
||||||
|
|
||||||
|
def get_current_config(self):
|
||||||
|
return {key: cb.currentText() for key, cb in self.comboboxes.items()}
|
||||||
|
|
||||||
|
def send_command(self, command):
|
||||||
|
# self.uart_logic.send_command(command["hex_string"])
|
||||||
|
self.uart_logic.send_pgkomm2(command["hex_string"])
|
||||||
|
config.CURRENT_COMMAND = command["name"]
|
||||||
|
|
||||||
|
def send_command_raw(self):
|
||||||
|
hex_str = self.input_hex.text().strip()
|
||||||
|
if not hex_str:
|
||||||
|
self.console.log("warning", "⚠️ No command entered.")
|
||||||
|
else:
|
||||||
|
self.uart_logic.send_command(hex_str)
|
||||||
|
config.CURRENT_COMMAND = hex_str
|
||||||
Loading…
Reference in new issue