diff --git a/buffer_kit/buffer_widget_compact.py b/buffer_kit/buffer_widget_compact.py
index 00faaca..708c98e 100644
--- a/buffer_kit/buffer_widget_compact.py
+++ b/buffer_kit/buffer_widget_compact.py
@@ -17,6 +17,26 @@ from .circular_buffer import (
)
+def format_bytes(b: int) -> str:
+ """
+ Format byte count as human-readable string.
+
+ Args:
+ b: Byte count
+
+ Returns:
+ Formatted string (e.g., "120.5KB", "400.0MB", "1.2GB")
+ """
+ if b >= 1024**3: # GB
+ return f"{b / (1024**3):.1f}GB"
+ elif b >= 1024**2: # MB
+ return f"{b / (1024**2):.1f}MB"
+ elif b >= 1024: # KB
+ return f"{b / 1024:.1f}KB"
+ else:
+ return f"{b}B"
+
+
class CompactBufferWidget(QWidget):
"""
Compact one-line buffer display.
@@ -67,9 +87,9 @@ class CompactBufferWidget(QWidget):
fill = cb_fill_bytes(self.buffer)
pct = cb_fill_pct(self.buffer)
ovf = cb_overflows(self.buffer)
-
- # Update labels
- self.lbl_size.setText(f"{fill}/{cap}B")
+
+ # Update labels with human-readable format
+ self.lbl_size.setText(f"{format_bytes(fill)}/{format_bytes(cap)}")
self.progress.setValue(pct)
# Overflow label - just show count (no color logic)
@@ -138,7 +158,7 @@ class CompactBufferWidgetWithCallback(QWidget):
def update_display(self):
"""
Update display and call callback if set.
-
+
Callback signature:
callback(capacity: int, fill: int, percent: int, overflows: int)
"""
@@ -146,9 +166,9 @@ class CompactBufferWidgetWithCallback(QWidget):
fill = cb_fill_bytes(self.buffer)
pct = cb_fill_pct(self.buffer)
ovf = cb_overflows(self.buffer)
-
- # Update UI
- self.lbl_size.setText(f"{fill}/{cap}B")
+
+ # Update UI with human-readable format
+ self.lbl_size.setText(f"{format_bytes(fill)}/{format_bytes(cap)}")
self.progress.setValue(pct)
# Overflow label - just show count
diff --git a/database/ehinge.db b/database/ehinge.db
index a4c1f84..6dfdc40 100644
Binary files a/database/ehinge.db and b/database/ehinge.db differ
diff --git a/run.py b/run.py
index 5a8c254..ecdc298 100644
--- a/run.py
+++ b/run.py
@@ -27,9 +27,11 @@ from uart.uart_kit.uart_core import (
PacketConfig,
PacketInfo,
uart_write,
+ uart_send_and_read_pgkomm2,
uart_start_listening_with_packets,
uart_stop_listening,
uart_get_detected_packets,
+ uart_get_packet_errors,
uart_clear_detected_packets,
uart_read_buffer,
Status as UARTStatus
@@ -42,6 +44,9 @@ from i2c.i2c_kit.i2c_core import (
Status as I2CStatus
)
+# Import buffer utilities
+from buffer_kit.circular_buffer import cb_fill_bytes, cb_capacity
+
# Import decoder
from decoder import decode_uart_packet, decode_i2c_sample
@@ -81,7 +86,10 @@ class RunExecutor:
uart_logger_port: Optional[UARTPort],
i2c_port: Optional[I2CHandle],
packet_config: PacketConfig,
+ i2c_address: int = 0x40,
+ i2c_register: int = 0xFE,
stop_timeout_ms: int = 5000,
+ grace_timeout_ms: int = 1500,
raw_data_callback = None
) -> Tuple[str, int, str]:
"""
@@ -120,23 +128,36 @@ class RunExecutor:
# ================================================================
if uart_logger_port and packet_config.enable:
+ # Debug: Check if I2C is available
+ if raw_data_callback:
+ if i2c_port:
+ raw_data_callback("INFO", f"I2C enabled: will trigger reads on packet detection")
+ else:
+ raw_data_callback("INFO", f"I2C disabled: no I2C port available")
+
# Create callback for I2C triggering
+ callback_count = [0] # Use list for mutable counter in nested function
+
def on_uart_packet_detected(timestamp_ns: int):
"""
Called immediately when UART packet detected.
Triggers I2C read for timestamp correlation.
"""
+ callback_count[0] += 1
+
+ # Debug first few callbacks
+ if callback_count[0] <= 3:
+ print(f"[DEBUG] I2C callback triggered #{callback_count[0]} at {timestamp_ns}")
+
if i2c_port:
# Read I2C angle immediately
- # Note: i2c_read_block requires (handle, addr, reg, length)
- # But we're using the handle's default address
status, i2c_bytes = i2c_read_block(
i2c_port,
- i2c_port.config.address, # Use configured address
- 0xFE, # Angle register
+ i2c_address, # Device address from session config
+ i2c_register, # Register address from session config
2 # Read 2 bytes
)
-
+
if status == I2CStatus.OK:
# Store with correlated timestamp
self.i2c_readings.append({
@@ -146,7 +167,9 @@ class RunExecutor:
else:
# I2C read failed - count the failure
self.i2c_failures += 1
-
+ if callback_count[0] <= 3:
+ print(f"[DEBUG] I2C read failed: {status}")
+
# Create packet config with callback
packet_config_with_callback = PacketConfig(
enable=packet_config.enable,
@@ -155,7 +178,12 @@ class RunExecutor:
end_marker=packet_config.end_marker,
on_packet_callback=on_uart_packet_detected if i2c_port else None
)
-
+
+ # Debug: Verify callback is attached
+ if raw_data_callback:
+ has_callback = packet_config_with_callback.on_packet_callback is not None
+ raw_data_callback("INFO", f"Packet config: callback={'attached' if has_callback else 'None'}")
+
# Start listening with packet detection on LOGGER PORT
status = uart_start_listening_with_packets(uart_logger_port, packet_config_with_callback)
@@ -163,99 +191,190 @@ class RunExecutor:
return ("error", 0, "Failed to start UART packet detection")
# ================================================================
- # 2. Send UART command (COMMAND PORT)
+ # 2. Send UART command (COMMAND PORT) - Using PGKomm2
# ================================================================
-
+
# Parse hex string to bytes
command_bytes = self._parse_hex_string(command_hex)
if not command_bytes:
if uart_logger_port:
uart_stop_listening(uart_logger_port)
return ("error", 0, f"Invalid command hex string: {command_hex}")
-
- # Send command via COMMAND PORT
- status, written = uart_write(uart_command_port, command_bytes)
-
+
+ # Emit TX data (command to be sent)
+ if raw_data_callback:
+ hex_tx = ' '.join(f'{b:02X}' for b in command_bytes)
+ # Add ASCII (skip DD 22 magic bytes)
+ ascii_data = command_bytes[2:] if len(command_bytes) >= 2 else command_bytes
+ ascii_tx = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in ascii_data)
+ raw_data_callback("TX", f"{hex_tx} | '{ascii_tx}'")
+
+ # Send command via PGKomm2 (always use this mode for sessions)
+ status, frames = uart_send_and_read_pgkomm2(
+ uart_command_port,
+ command_bytes,
+ capture_max_ms=30, # Default PGKomm2 timeout
+ log_callback=raw_data_callback # Pass callback for logging
+ )
+
if status != UARTStatus.OK:
if uart_logger_port:
uart_stop_listening(uart_logger_port)
- return ("error", 0, "Failed to send UART command")
-
- # Emit TX data (command sent)
- if raw_data_callback:
- hex_tx = ' '.join(f'{b:02X}' for b in command_bytes)
- raw_data_callback("TX", hex_tx)
-
- # ================================================================
- # 3. Wait for stop condition
- # ================================================================
-
- # Wait for timeout
- time.sleep(stop_timeout_ms / 1000.0)
-
- # ================================================================
- # 3.5. Handle raw data if packet detection disabled
- # ================================================================
-
- if not packet_config.enable:
- # No packet detection - read raw buffer from COMMAND PORT (ACK/response)
- status_read, raw_data = uart_read_buffer(uart_command_port)
-
- if status_read == UARTStatus.OK and raw_data:
- # Emit RX data
- if raw_data_callback:
- hex_rx = ' '.join(f'{b:02X}' for b in raw_data)
- raw_data_callback("RX", hex_rx)
-
- # Stop listening on logger port (if active)
- if uart_logger_port:
- uart_stop_listening(uart_logger_port)
-
+ return ("error", 0, f"PGKomm2 command failed: {status}")
+
+ # Emit RX data (frames received) - only show Echo and Response, skip SB broadcasts
+ if raw_data_callback and frames:
+ for frame in frames:
+ if len(frame) >= 5:
+ adr1, adr2 = frame[2], frame[3]
+
+ # Skip SB status broadcasts (background noise from device)
+ if adr1 == 0x53 and adr2 == 0x42: # SB
+ continue
+
+ hex_rx = ' '.join(f'{b:02X}' for b in frame)
+ # Add ASCII (skip DD 22 magic bytes)
+ ascii_data = frame[2:] if len(frame) >= 2 else frame
+ ascii_rx = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in ascii_data)
+
+ if adr1 == 0x50 and adr2 == 0x48: # PH echo
+ raw_data_callback("RX", f"{hex_rx} (Echo) | '{ascii_rx}'")
+ elif adr1 == 0x48 and adr2 == 0x50: # HP response
+ raw_data_callback("RX", f"{hex_rx} (Response) | '{ascii_rx}'")
+ else:
+ raw_data_callback("RX", f"{hex_rx} | '{ascii_rx}'")
+ else:
+ # Unknown frame format
+ hex_rx = ' '.join(f'{b:02X}' for b in frame)
+ ascii_data = frame[2:] if len(frame) >= 2 else frame
+ ascii_rx = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in ascii_data)
+ raw_data_callback("RX", f"{hex_rx} | '{ascii_rx}'")
+
# ================================================================
- # 4. Get detected packets (from LOGGER PORT if exists)
+ # 3. Wait for logger packets (polling mode with stop condition)
# ================================================================
-
+
uart_packets = []
- if uart_logger_port:
- uart_packets = uart_get_detected_packets(uart_logger_port)
+
+ if uart_logger_port and packet_config.enable:
+ # Polling mode: wait for packets with grace period and timeout
+ # Use defaults if None from database
+ grace_ms = grace_timeout_ms if grace_timeout_ms is not None else 1500
+ stop_ms = stop_timeout_ms if stop_timeout_ms is not None else 150
+
+ grace_timeout_s = grace_ms / 1000.0 # Wait for first packet
+ stop_timeout_s = stop_ms / 1000.0 # Silence between packets
+
+ last_packet_count = 0
+ last_packet_time = 0.0
+ start_time = time.time()
+ first_packet_received = False
+
+ if raw_data_callback:
+ raw_data_callback("INFO", f"Waiting for logger packets (grace: {grace_timeout_s*1000:.0f}ms, timeout: {stop_timeout_s*1000:.0f}ms)...")
+
+ # Polling loop
+ while True:
+ time.sleep(0.05) # Poll every 50ms
+ current_time = time.time()
+
+ # Get current packet count
+ current_packets = uart_get_detected_packets(uart_logger_port)
+ current_count = len(current_packets)
+
+ # Check if new packets arrived
+ if current_count > last_packet_count:
+ last_packet_count = current_count
+ last_packet_time = current_time
+ if not first_packet_received:
+ first_packet_received = True
+ if raw_data_callback:
+ raw_data_callback("INFO", f"First logger packet received, monitoring for stop condition...")
+
+ # Grace period check (only if no packets yet)
+ if not first_packet_received:
+ elapsed = current_time - start_time
+ if elapsed >= grace_timeout_s:
+ # Grace period expired, no packets
+ uart_stop_listening(uart_logger_port)
+ return ("error", 0, f"Logger not responding (grace timeout: {grace_timeout_s*1000:.0f}ms)")
+
+ # Stop timeout check (only after first packet received)
+ if first_packet_received:
+ silence = current_time - last_packet_time
+ if silence >= stop_timeout_s:
+ # Stop condition met!
+ if raw_data_callback:
+ # Report buffer status
+ if uart_logger_port._rx_buffer:
+ fill = cb_fill_bytes(uart_logger_port._rx_buffer)
+ cap = cb_capacity(uart_logger_port._rx_buffer)
+ fill_mb = fill / (1024 * 1024)
+ cap_mb = cap / (1024 * 1024)
+ raw_data_callback("INFO", f"Buffer: {fill_mb:.2f}MB / {cap_mb:.1f}MB")
+
+ # Report packet statistics
+ packet_errors = uart_get_packet_errors(uart_logger_port)
+ if packet_errors > 0:
+ raw_data_callback("ERROR", f"⚠ Packet errors: {packet_errors} packets with end marker mismatch")
+ raw_data_callback("INFO", f"✓ Valid packets: {current_count}")
+ raw_data_callback("INFO", f"Stop condition: {stop_timeout_s*1000:.0f}ms silence detected")
+
+ # Stop listening (but keep port open for next command)
+ uart_stop_listening(uart_logger_port)
+ uart_packets = current_packets
+ break
+
+ packet_count = len(uart_packets)
+
+ elif uart_logger_port:
+ # Logger enabled but packet detection disabled - just stop listening
+ uart_stop_listening(uart_logger_port)
+ packet_count = 0
+
else:
- uart_packets = []
- packet_count = len(uart_packets)
-
- if packet_count == 0 and packet_config.enable:
- # Only error if packet detection was enabled
- return ("error", 0, "No packets detected (timeout or no data)")
-
+ # No logger port
+ packet_count = 0
+
# ================================================================
- # 5. Decode and save data
+ # 4. Decode and save data
# ================================================================
-
- # Decode and save UART packets
+
+ if raw_data_callback:
+ i2c_count = len(self.i2c_readings)
+ if i2c_count == 0 and i2c_port and packet_config.enable:
+ # Expected I2C but got none - report
+ raw_data_callback("ERROR", f"⚠ No I2C readings captured (expected ~{packet_count})")
+ if self.i2c_failures > 0:
+ raw_data_callback("ERROR", f"I2C failures: {self.i2c_failures}")
+ raw_data_callback("INFO", f"Decoding and saving {packet_count} UART packets + {i2c_count} I2C readings...")
+
+ # Create timestamp → I2C reading map for matching
+ i2c_by_timestamp = {}
+ for reading in self.i2c_readings:
+ i2c_by_timestamp[reading['timestamp_ns']] = reading['i2c_bytes']
+
+ # Decode and save UART packets WITH correlated I2C data
for pkt in uart_packets:
- self._save_uart_telemetry(
+ # Look up matching I2C reading by timestamp
+ i2c_bytes = i2c_by_timestamp.get(pkt.start_timestamp, None)
+
+ self._save_combined_telemetry(
session_id=session_id,
session_name=session_name,
run_no=run_no,
run_command_id=command_id,
packet_info=pkt,
+ i2c_bytes=i2c_bytes,
run_start_ns=run_start_ns
)
-
- # Decode and save I2C readings
- for reading in self.i2c_readings:
- self._save_i2c_telemetry(
- session_id=session_id,
- session_name=session_name,
- run_no=run_no,
- run_command_id=command_id,
- timestamp_ns=reading['timestamp_ns'],
- i2c_bytes=reading['i2c_bytes'],
- run_start_ns=run_start_ns
- )
-
+
# Commit database changes
self.db_conn.commit()
+ if raw_data_callback:
+ raw_data_callback("INFO", f"✓ Database saved: {packet_count} UART packets, {len(self.i2c_readings)} I2C readings")
+
# Report errors if any via callback
if self.i2c_failures > 0 and raw_data_callback:
raw_data_callback("ERROR", f"I2C read failures: {self.i2c_failures}")
@@ -289,33 +408,36 @@ class RunExecutor:
except:
return None
- def _save_uart_telemetry(
+ def _save_combined_telemetry(
self,
session_id: str,
session_name: str,
run_no: int,
run_command_id: int,
packet_info: PacketInfo,
+ i2c_bytes: Optional[bytes],
run_start_ns: int
):
"""
- Save UART telemetry to database.
-
+ Save combined UART + I2C telemetry to database (single row).
+
Saves to both telemetry_raw and telemetry_decoded tables.
+ UART and I2C data are correlated by timestamp and saved together.
"""
- # Decode packet
- decoded = decode_uart_packet(packet_info.data)
-
+ # Decode packets
+ decoded_uart = decode_uart_packet(packet_info.data)
+ decoded_i2c = decode_i2c_sample(i2c_bytes) if i2c_bytes else None
+
# Calculate relative time from run start
time_ms = (packet_info.start_timestamp - run_start_ns) / 1_000_000.0
-
- # Save to telemetry_raw (backup)
+
+ # Save to telemetry_raw (backup) - BOTH uart_raw_packet AND i2c_raw_bytes in ONE row
cursor = self.db_conn.cursor()
cursor.execute("""
INSERT INTO telemetry_raw (
session_id, session_name, run_no, run_command_id,
- t_ns, time_ms, uart_raw_packet
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
+ t_ns, time_ms, uart_raw_packet, i2c_raw_bytes
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
session_id,
session_name,
@@ -323,11 +445,12 @@ class RunExecutor:
run_command_id,
packet_info.start_timestamp,
time_ms,
- packet_info.data
+ packet_info.data,
+ i2c_bytes # Can be None if no I2C
))
-
+
# Save to telemetry_decoded (main data)
- # For now, just save raw hex (decoder is pass-through)
+ # For now, just save timestamps (decoder is pass-through)
# TODO: Update when decoder is fully implemented
cursor.execute("""
INSERT INTO telemetry_decoded (
@@ -342,67 +465,10 @@ class RunExecutor:
packet_info.start_timestamp,
time_ms
))
-
- # TODO: When decoder is fully implemented, also save:
- # motor_current, encoder_value, relative_encoder_value, v24_pec_diff, pwm
-
- def _save_i2c_telemetry(
- self,
- session_id: str,
- session_name: str,
- run_no: int,
- run_command_id: int,
- timestamp_ns: int,
- i2c_bytes: bytes,
- run_start_ns: int
- ):
- """
- Save I2C telemetry to database.
-
- Saves to both telemetry_raw and telemetry_decoded tables.
- """
- # Decode I2C sample
- decoded = decode_i2c_sample(i2c_bytes)
-
- # Calculate relative time from run start
- time_ms = (timestamp_ns - run_start_ns) / 1_000_000.0
-
- # Save to telemetry_raw (backup)
- cursor = self.db_conn.cursor()
- cursor.execute("""
- INSERT INTO telemetry_raw (
- session_id, session_name, run_no, run_command_id,
- t_ns, time_ms, i2c_raw_bytes
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
- """, (
- session_id,
- session_name,
- run_no,
- run_command_id,
- timestamp_ns,
- time_ms,
- i2c_bytes
- ))
-
- # Save to telemetry_decoded (main data)
- # For now, decoder is pass-through
- # TODO: Update when decoder is fully implemented with angle conversion
- cursor.execute("""
- INSERT INTO telemetry_decoded (
- session_id, session_name, run_no, run_command_id,
- t_ns, time_ms
- ) VALUES (?, ?, ?, ?, ?, ?)
- """, (
- session_id,
- session_name,
- run_no,
- run_command_id,
- timestamp_ns,
- time_ms
- ))
-
+
# TODO: When decoder is fully implemented, also save:
- # i2c_raw14, i2c_angle_deg, i2c_zero_raw14, etc.
+ # UART: motor_current, encoder_value, relative_encoder_value, v24_pec_diff, pwm
+ # I2C: i2c_raw14, i2c_angle_deg, i2c_zero_raw14, etc.
# =============================================================================
@@ -420,12 +486,15 @@ def execute_run(
uart_logger_port: Optional[UARTPort],
i2c_port: Optional[I2CHandle],
packet_config: PacketConfig,
+ i2c_address: int = 0x40,
+ i2c_register: int = 0xFE,
stop_timeout_ms: int = 5000,
+ grace_timeout_ms: int = 1500,
raw_data_callback = None
) -> Tuple[str, int, str]:
"""
Execute a single RUN (convenience function).
-
+
Args:
db_connection: Database connection
session_id: Session ID
@@ -438,8 +507,9 @@ def execute_run(
i2c_port: I2C port (optional)
packet_config: Packet detection configuration
stop_timeout_ms: Stop condition timeout
+ grace_timeout_ms: Grace period before first packet
raw_data_callback: Callback for raw data display (direction, hex_string)
-
+
Returns:
(status, packet_count, error_msg)
"""
@@ -454,7 +524,10 @@ def execute_run(
uart_logger_port=uart_logger_port,
i2c_port=i2c_port,
packet_config=packet_config,
+ i2c_address=i2c_address,
+ i2c_register=i2c_register,
stop_timeout_ms=stop_timeout_ms,
+ grace_timeout_ms=grace_timeout_ms,
raw_data_callback=raw_data_callback
)
diff --git a/session.py b/session.py
index 92234c1..31a2de4 100644
--- a/session.py
+++ b/session.py
@@ -761,7 +761,10 @@ class Session(QObject):
uart_logger_port=self.uart_logger_port,
i2c_port=self.i2c_handle,
packet_config=self.packet_config,
+ i2c_address=int(self.interface_config['i2c_slave_address'], 16) if self.interface_config.get('i2c_slave_address') else 0x40,
+ i2c_register=int(self.interface_config['i2c_slave_read_register'], 16) if self.interface_config.get('i2c_slave_read_register') else 0xFE,
stop_timeout_ms=self.interface_config['uart_logger_timeout_ms'],
+ grace_timeout_ms=self.interface_config['uart_logger_grace_ms'],
raw_data_callback=lambda direction, hex_str: self.raw_data_received.emit(direction, hex_str)
)
diff --git a/session_widget.py b/session_widget.py
index 54ed0cb..b272433 100644
--- a/session_widget.py
+++ b/session_widget.py
@@ -529,18 +529,24 @@ class SessionWidget(QWidget):
def _on_raw_data_received(self, direction: str, hex_string: str):
"""
Handle raw UART data display.
-
+
Args:
- direction: "TX" or "RX"
- hex_string: Hex bytes (e.g., "EF FE 01 02 03")
+ direction: "TX", "RX", or "ERROR"
+ hex_string: Hex bytes (e.g., "EF FE 01 02 03") or error message
"""
if direction == "TX":
color = "green"
prefix = "→ TX"
+ elif direction == "ERROR":
+ color = "red"
+ prefix = "✗ ERROR"
+ elif direction == "INFO":
+ color = "gray"
+ prefix = "ℹ INFO"
else:
color = "blue"
prefix = "← RX"
-
+
self.log_display.append(
f"[{prefix}] {hex_string}"
)
diff --git a/uart/uart_kit/uart_core.py b/uart/uart_kit/uart_core.py
index 9b7a780..7a3c00f 100644
--- a/uart/uart_kit/uart_core.py
+++ b/uart/uart_kit/uart_core.py
@@ -80,8 +80,9 @@ __all__ = [
# NEW: Listening with packet detection
'uart_start_listening_with_packets',
'uart_get_detected_packets',
+ 'uart_get_packet_errors',
'uart_clear_detected_packets',
-
+
# Status
'uart_get_status',
]
@@ -136,8 +137,8 @@ class UARTConfig:
baudrate: int
data_bits: int = 8
stop_bits: int = 1
- parity: str = 'N'
- buffer_size: int = 40 * 1024 * 1024 # 40MB default buffer
+ parity: str = 'E'
+ buffer_size: int = 0.256 * 1024 * 1024 # 4MB default buffer
read_chunk_size: int = 512
stop_mode: StopConditionMode = StopConditionMode.TIMEOUT
@@ -263,6 +264,7 @@ class UARTPort:
_packet_buffer: bytearray = field(default_factory=bytearray)
_packet_detection_active: bool = False
_packet_start_timestamp: float = 0.0
+ _packet_errors: int = 0 # Count of packets with end marker errors
# Timestamp function
_get_timestamp: Callable[[], float] = field(default=time.perf_counter)
@@ -482,13 +484,13 @@ def _reader_thread_func(port: UARTPort) -> None:
def _detect_packets_in_chunk(port: UARTPort, chunk: bytes, timestamp: float) -> None:
"""
Detect packets in received chunk.
-
+
Uses configured packet format (start marker, length, end marker).
Stores complete packets in _detected_packets list with timestamps.
-
+
Packet format: [START_MARKER][DATA][END_MARKER]
Example: EF FE [14 bytes] EE
-
+
Args:
port: UART port instance
chunk: Received data chunk
@@ -496,24 +498,27 @@ def _detect_packets_in_chunk(port: UARTPort, chunk: bytes, timestamp: float) ->
"""
if not port._packet_config or not port._packet_config.enable:
return
-
+
cfg = port._packet_config
-
+
# Add chunk to packet buffer
port._packet_buffer.extend(chunk)
-
+
# Process buffer looking for complete packets
while len(port._packet_buffer) >= (cfg.packet_length or 0):
# Look for start marker
if cfg.start_marker:
# Find start marker position
start_idx = port._packet_buffer.find(cfg.start_marker)
-
+
if start_idx == -1:
- # No start marker found - clear old data, keep last few bytes
- # (in case start marker is split across chunks)
- if len(port._packet_buffer) > 100:
- port._packet_buffer = port._packet_buffer[-10:]
+ # No start marker found - keep searching in larger buffer
+ # Only clear if buffer gets excessively large (prevent memory issues)
+ max_buffer_size = 1024 * 1024 # 1MB should be plenty for packet detection
+ if len(port._packet_buffer) > max_buffer_size:
+ # Keep last chunk that might contain start of next packet
+ keep_size = min(cfg.packet_length * 2, 1024)
+ port._packet_buffer = port._packet_buffer[-keep_size:]
break
# Remove everything before start marker
@@ -533,7 +538,8 @@ def _detect_packets_in_chunk(port: UARTPort, chunk: bytes, timestamp: float) ->
actual_end = packet_bytes[expected_end_pos:]
if actual_end != cfg.end_marker:
- # Invalid packet - discard first byte and try again
+ # Invalid packet - count error, discard first byte and try again
+ port._packet_errors += 1
port._packet_buffer.pop(0)
continue
@@ -541,7 +547,7 @@ def _detect_packets_in_chunk(port: UARTPort, chunk: bytes, timestamp: float) ->
# Timestamp at packet START (when we found start marker)
if port._packet_start_timestamp == 0.0:
port._packet_start_timestamp = timestamp
-
+
# Create packet info
with port._lock:
port._total_packets += 1
@@ -562,9 +568,11 @@ def _detect_packets_in_chunk(port: UARTPort, chunk: bytes, timestamp: float) ->
if cfg.on_packet_callback:
try:
cfg.on_packet_callback(packet_info.start_timestamp)
- except Exception:
+ except Exception as e:
# Don't crash reader thread if callback fails
- pass
+ print(f"[ERROR] Packet callback exception: {e}")
+ import traceback
+ traceback.print_exc()
# Remove packet from buffer
port._packet_buffer = port._packet_buffer[cfg.packet_length:]
@@ -595,9 +603,11 @@ def _detect_packets_in_chunk(port: UARTPort, chunk: bytes, timestamp: float) ->
if cfg.on_packet_callback:
try:
cfg.on_packet_callback(packet_info.start_timestamp)
- except Exception:
+ except Exception as e:
# Don't crash reader thread if callback fails
- pass
+ print(f"[ERROR] Packet callback exception: {e}")
+ import traceback
+ traceback.print_exc()
port._packet_buffer = port._packet_buffer[cfg.packet_length:]
else:
@@ -690,7 +700,8 @@ def uart_send_and_receive(port: UARTPort, tx_data: bytes,
def uart_send_and_read_pgkomm2(port: UARTPort,
tx_data: bytes,
capture_max_ms: int = 30,
- max_frames: int = 10) -> Tuple[Status, Optional[list]]:
+ max_frames: int = 10,
+ log_callback = None) -> Tuple[Status, Optional[list]]:
"""
Send PGKomm2 command and read response frames.
@@ -713,6 +724,7 @@ def uart_send_and_read_pgkomm2(port: UARTPort,
tx_data: PGKomm2 command to transmit (must start with DD 22)
capture_max_ms: Total capture window in ms (default 30ms, spec says <15ms but real-world needs margin)
max_frames: Maximum number of frames to parse (safety limit)
+ log_callback: Optional callback(direction, message) for logging (e.g., to GUI)
Returns:
(Status.OK, [frame1, frame2, ...]) on success
@@ -779,7 +791,7 @@ def uart_send_and_read_pgkomm2(port: UARTPort,
# Try to extract complete frames as they arrive
while True:
- frame = _extract_pgkomm2_frame(rx_buffer)
+ frame = _extract_pgkomm2_frame(rx_buffer, log_callback)
if frame is None:
break # Need more bytes
@@ -816,15 +828,27 @@ def uart_send_and_read_pgkomm2(port: UARTPort,
return (Status.OK, collected_frames)
elif len(collected_frames) > 0:
# Got frames but no HP response (only SB broadcasts or PH echo without answer)
- print(f"[PGKOMM2] TIMEOUT: Got {len(collected_frames)} frame(s) but no HP response")
+ msg = f"[PGKOMM2] TIMEOUT: Got {len(collected_frames)} frame(s) but no HP response"
+ if log_callback:
+ log_callback("ERROR", msg)
+ else:
+ print(msg)
return (Status.TIMEOUT, None)
elif len(rx_buffer) > 0:
# Unparseable data - log for debugging
- print(f"[PGKOMM2] IO_ERROR: Unparsed buffer ({len(rx_buffer)} bytes): {rx_buffer.hex(' ').upper()}")
+ msg = f"[PGKOMM2] IO_ERROR: Unparsed buffer ({len(rx_buffer)} bytes): {rx_buffer.hex(' ').upper()}"
+ if log_callback:
+ log_callback("ERROR", msg)
+ else:
+ print(msg)
return (Status.IO_ERROR, None)
else:
# No response
- print(f"[PGKOMM2] TIMEOUT: No data received within {capture_max_ms}ms")
+ msg = f"[PGKOMM2] TIMEOUT: No data received within {capture_max_ms}ms"
+ if log_callback:
+ log_callback("ERROR", msg)
+ else:
+ print(msg)
return (Status.TIMEOUT, None)
except Exception as e:
@@ -843,7 +867,7 @@ def uart_send_and_read_pgkomm2(port: UARTPort,
uart_start_reader(port)
-def _extract_pgkomm2_frame(buffer: bytearray) -> Optional[bytes]:
+def _extract_pgkomm2_frame(buffer: bytearray, log_callback=None) -> Optional[bytes]:
"""
Extract ONE complete PGKomm2 frame from buffer (destructive).
@@ -857,6 +881,7 @@ def _extract_pgkomm2_frame(buffer: bytearray) -> Optional[bytes]:
Args:
buffer: Bytearray to extract from (will be modified!)
+ log_callback: Optional callback(direction, message) for logging
Returns:
Complete frame as bytes, or None if no complete frame available
@@ -896,8 +921,17 @@ def _extract_pgkomm2_frame(buffer: bytearray) -> Optional[bytes]:
if calculated_bcc != received_bcc:
# BCC mismatch - frame corrupted!
adr_str = f"{frame[2]:02X} {frame[3]:02X}" if len(frame) >= 4 else "??"
- print(f"[PGKOMM2] ✗ BCC FAIL: ADR={adr_str}, calc={calculated_bcc:02X}, recv={received_bcc:02X}")
- print(f"[PGKOMM2] Frame: {frame.hex(' ').upper()}")
+ error_msg = f"[PGKOMM2] ✗ BCC FAIL: ADR={adr_str}, calc={calculated_bcc:02X}, recv={received_bcc:02X}"
+ frame_msg = f"[PGKOMM2] Frame: {frame.hex(' ').upper()}"
+
+ # Log via callback or print
+ if log_callback:
+ log_callback("ERROR", error_msg)
+ log_callback("ERROR", frame_msg)
+ else:
+ print(error_msg)
+ print(frame_msg)
+
# Drop this frame and continue searching
del buffer[:i + total]
return None # Reject corrupted frame
@@ -1064,7 +1098,8 @@ def uart_start_listening_with_packets(port: UARTPort, packet_config: PacketConfi
port._packet_config = packet_config
port._packet_buffer.clear()
port._packet_start_timestamp = 0.0
-
+ port._packet_errors = 0 # Reset error counter
+
# Mark listening start time
with port._lock:
port._listening_start_time = port._get_timestamp()
@@ -1096,6 +1131,16 @@ def uart_get_detected_packets(port: UARTPort) -> list:
return port._detected_packets.copy()
+def uart_get_packet_errors(port: UARTPort) -> int:
+ """
+ Get number of packet errors (end marker mismatches).
+
+ Returns:
+ Number of packets with end marker errors since listening started
+ """
+ return port._packet_errors
+
+
def uart_clear_detected_packets(port: UARTPort) -> Status:
"""
Clear detected packets list.
@@ -1108,10 +1153,11 @@ def uart_clear_detected_packets(port: UARTPort) -> Status:
port._detected_packets.clear()
port._packet_buffer.clear()
port._packet_start_timestamp = 0.0
-
+ port._packet_errors = 0 # Reset error counter
+
with port._lock:
port._total_packets = 0
-
+
return Status.OK
diff --git a/uart/uart_kit/uart_core_widget.py b/uart/uart_kit/uart_core_widget.py
index 3273623..3c8f2e4 100644
--- a/uart/uart_kit/uart_core_widget.py
+++ b/uart/uart_kit/uart_core_widget.py
@@ -35,7 +35,7 @@ from PyQt6.QtGui import QFont
# Import UART core and buffer widget
from uart.uart_kit.uart_core import *
from buffer_kit.buffer_widget_compact import CompactBufferWidget
-from buffer_kit.circular_buffer import cb_overflows
+from buffer_kit.circular_buffer import cb_overflows, cb_debug_info, cb_fill_bytes, cb_capacity
class UARTCommandWorker(QThread):
@@ -101,10 +101,14 @@ class UARTWidget(QWidget):
# Overflow tracking (for Data Monitor warnings)
self._last_overflow_count = 0
-
+
# Packet detection state (NEW)
self._packet_detection_enabled = False
self._detected_packet_count = 0
+
+ # Polling mode timeout tracking (for auto-stop)
+ self._last_packet_time = 0.0
+ self._polling_start_time = 0.0
# Build UI
self._init_ui()
@@ -301,10 +305,10 @@ class UARTWidget(QWidget):
row3.addWidget(QLabel("Length:"))
self.spin_packet_length = QSpinBox()
self.spin_packet_length.setRange(1, 1024)
- self.spin_packet_length.setValue(17)
+ self.spin_packet_length.setValue(14) # Default: 14 bytes (EF FE + 11 data + EE)
self.spin_packet_length.setSuffix(" B")
self.spin_packet_length.setMaximumWidth(80)
- self.spin_packet_length.setToolTip("Total packet length in bytes")
+ self.spin_packet_length.setToolTip("Total packet length in bytes (including start+data+end)")
row3.addWidget(self.spin_packet_length)
# End marker
@@ -318,14 +322,14 @@ class UARTWidget(QWidget):
self.lbl_packet_count = QLabel("Packets: 0")
self.lbl_packet_count.setStyleSheet("color: gray; font-weight: bold;")
row3.addWidget(self.lbl_packet_count)
-
+
# Stretch at end
row3.addStretch()
-
+
# Initially disabled (only for Listening mode)
self.check_packet_detect.setEnabled(False)
self._on_packet_detect_toggled(0) # Disable fields
-
+
main_layout.addLayout(row3)
# Update UI based on initial mode
@@ -416,7 +420,7 @@ class UARTWidget(QWidget):
# Listening mode update timer
self.listen_timer = QTimer()
self.listen_timer.timeout.connect(self._update_listening_display)
-
+
# Polling mode timer
self.poll_timer = QTimer()
self.poll_timer.timeout.connect(self._poll_next_packet)
@@ -515,7 +519,7 @@ class UARTWidget(QWidget):
data_bits=int(self.combo_databits.currentText()),
stop_bits=int(self.combo_stopbits.currentText()),
parity='N' if self.combo_parity.currentText() == 'None' else self.combo_parity.currentText()[0],
- buffer_size=4096,
+ # buffer_size uses default from UARTConfig
stop_mode=stop_mode,
stop_timeout_ms=self.spin_timeout.value(),
stop_terminator=terminator,
@@ -603,6 +607,10 @@ class UARTWidget(QWidget):
# Start listening with packet detection
uart_start_listening_with_packets(self.port, packet_config)
self._packet_detection_enabled = True
+
+ # Initialize timeout tracking (0 = no packets yet, timeout not started)
+ self._last_packet_time = 0.0
+
self.poll_timer.start(50) # Poll for packets
self._log_info(f"Polling mode started with packet detection")
self._log_info(f" Start: {start_marker.hex(' ')}, Length: {packet_length}, End: {end_marker.hex(' ') if end_marker else 'None'}")
@@ -610,11 +618,17 @@ class UARTWidget(QWidget):
except Exception as e:
self._show_error(f"Invalid packet config: {e}")
# Fallback to regular polling
+ # Initialize timeout tracking (0 = no packets yet, timeout not started)
+ self._last_packet_time = 0.0
+
self.poll_timer.start(50)
self._packet_detection_enabled = False
self._log_warning("Fallback to polling mode without packet detection")
else:
# Regular polling mode (use uart_poll_packet)
+ # Initialize timeout tracking (0 = no packets yet, timeout not started)
+ self._last_packet_time = 0.0
+
self.poll_timer.start(50)
self._packet_detection_enabled = False
self._log_info("Polling mode started (no packet detection)")
@@ -817,19 +831,48 @@ class UARTWidget(QWidget):
"""Poll for next packet in polling mode (NO printing to monitor)."""
if not self.port:
return
-
+
+ # Check timeout (auto-stop if no packets for timeout duration)
+ import time
+ current_time = time.time()
+ timeout_s = self.spin_timeout.value() / 1000.0 # Convert ms to seconds
+
+ if self._last_packet_time > 0: # Only check after first packet
+ silence_duration = current_time - self._last_packet_time
+ if silence_duration >= timeout_s:
+ # Timeout expired - print buffer info, packet stats, and auto-disconnect
+ if self.port and self.port._rx_buffer:
+ fill = cb_fill_bytes(self.port._rx_buffer)
+ cap = cb_capacity(self.port._rx_buffer)
+ fill_mb = fill / (1024 * 1024)
+ cap_mb = cap / (1024 * 1024)
+ self._log_info(f"Buffer status: {fill_mb:.2f}MB / {cap_mb:.1f}MB ({fill}/{cap} bytes)")
+
+ # Show packet statistics
+ if self.port:
+ total_packets = len(uart_get_detected_packets(self.port))
+ packet_errors = uart_get_packet_errors(self.port)
+ if packet_errors > 0:
+ self._log_error(f"⚠ Packet errors: {packet_errors} packets with end marker mismatch")
+ self._log_info(f"✓ Total valid packets: {total_packets}")
+
+ self._log_info(f"Auto-stop: {timeout_s*1000:.0f}ms timeout expired (no new packets)")
+ self._on_disconnect()
+ return
+
# Update buffer widget
if isinstance(self.buffer_widget, CompactBufferWidget):
self.buffer_widget.update_display()
-
+
# If packet detection is enabled, update packet count (NO printing)
if self._packet_detection_enabled:
packets = uart_get_detected_packets(self.port)
current_count = len(packets)
-
+
if current_count != self._detected_packet_count:
- # New packets detected - update counter only, NO printing
+ # New packets detected - update counter and timestamp
self._detected_packet_count = current_count
+ self._last_packet_time = current_time # Update timestamp
self.lbl_packet_count.setText(f"Packets: {current_count}")
self.lbl_packet_count.setStyleSheet("color: green; font-weight: bold;")
return
@@ -853,8 +896,11 @@ class UARTWidget(QWidget):
# Poll for packet (non-blocking)
status, packet = uart_poll_packet(self.port)
-
+
if status == Status.OK:
+ # Update packet timestamp
+ self._last_packet_time = current_time
+
# Display stop condition message in gray (logging mode only)
stop_msg = (
f""
@@ -865,10 +911,10 @@ class UARTWidget(QWidget):
f""
)
self.text_display.append(stop_msg)
-
+
# Update buffer widget
self.buffer_widget.update_display()
-
+
# Emit packet signal
self.packet_received.emit(packet)