Telemetry is wokring now on how to deocde it

main
Kynsight 4 weeks ago
parent ac173bf6f3
commit 70eb58531f

@ -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): class CompactBufferWidget(QWidget):
""" """
Compact one-line buffer display. Compact one-line buffer display.
@ -67,9 +87,9 @@ class CompactBufferWidget(QWidget):
fill = cb_fill_bytes(self.buffer) fill = cb_fill_bytes(self.buffer)
pct = cb_fill_pct(self.buffer) pct = cb_fill_pct(self.buffer)
ovf = cb_overflows(self.buffer) ovf = cb_overflows(self.buffer)
# Update labels # Update labels with human-readable format
self.lbl_size.setText(f"{fill}/{cap}B") self.lbl_size.setText(f"{format_bytes(fill)}/{format_bytes(cap)}")
self.progress.setValue(pct) self.progress.setValue(pct)
# Overflow label - just show count (no color logic) # Overflow label - just show count (no color logic)
@ -138,7 +158,7 @@ class CompactBufferWidgetWithCallback(QWidget):
def update_display(self): def update_display(self):
""" """
Update display and call callback if set. Update display and call callback if set.
Callback signature: Callback signature:
callback(capacity: int, fill: int, percent: int, overflows: int) callback(capacity: int, fill: int, percent: int, overflows: int)
""" """
@ -146,9 +166,9 @@ class CompactBufferWidgetWithCallback(QWidget):
fill = cb_fill_bytes(self.buffer) fill = cb_fill_bytes(self.buffer)
pct = cb_fill_pct(self.buffer) pct = cb_fill_pct(self.buffer)
ovf = cb_overflows(self.buffer) ovf = cb_overflows(self.buffer)
# Update UI # Update UI with human-readable format
self.lbl_size.setText(f"{fill}/{cap}B") self.lbl_size.setText(f"{format_bytes(fill)}/{format_bytes(cap)}")
self.progress.setValue(pct) self.progress.setValue(pct)
# Overflow label - just show count # Overflow label - just show count

Binary file not shown.

369
run.py

@ -27,9 +27,11 @@ from uart.uart_kit.uart_core import (
PacketConfig, PacketConfig,
PacketInfo, PacketInfo,
uart_write, uart_write,
uart_send_and_read_pgkomm2,
uart_start_listening_with_packets, uart_start_listening_with_packets,
uart_stop_listening, uart_stop_listening,
uart_get_detected_packets, uart_get_detected_packets,
uart_get_packet_errors,
uart_clear_detected_packets, uart_clear_detected_packets,
uart_read_buffer, uart_read_buffer,
Status as UARTStatus Status as UARTStatus
@ -42,6 +44,9 @@ from i2c.i2c_kit.i2c_core import (
Status as I2CStatus Status as I2CStatus
) )
# Import buffer utilities
from buffer_kit.circular_buffer import cb_fill_bytes, cb_capacity
# Import decoder # Import decoder
from decoder import decode_uart_packet, decode_i2c_sample from decoder import decode_uart_packet, decode_i2c_sample
@ -81,7 +86,10 @@ class RunExecutor:
uart_logger_port: Optional[UARTPort], uart_logger_port: Optional[UARTPort],
i2c_port: Optional[I2CHandle], i2c_port: Optional[I2CHandle],
packet_config: PacketConfig, packet_config: PacketConfig,
i2c_address: int = 0x40,
i2c_register: int = 0xFE,
stop_timeout_ms: int = 5000, stop_timeout_ms: int = 5000,
grace_timeout_ms: int = 1500,
raw_data_callback = None raw_data_callback = None
) -> Tuple[str, int, str]: ) -> Tuple[str, int, str]:
""" """
@ -120,23 +128,36 @@ class RunExecutor:
# ================================================================ # ================================================================
if uart_logger_port and packet_config.enable: 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 # Create callback for I2C triggering
callback_count = [0] # Use list for mutable counter in nested function
def on_uart_packet_detected(timestamp_ns: int): def on_uart_packet_detected(timestamp_ns: int):
""" """
Called immediately when UART packet detected. Called immediately when UART packet detected.
Triggers I2C read for timestamp correlation. 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: if i2c_port:
# Read I2C angle immediately # 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( status, i2c_bytes = i2c_read_block(
i2c_port, i2c_port,
i2c_port.config.address, # Use configured address i2c_address, # Device address from session config
0xFE, # Angle register i2c_register, # Register address from session config
2 # Read 2 bytes 2 # Read 2 bytes
) )
if status == I2CStatus.OK: if status == I2CStatus.OK:
# Store with correlated timestamp # Store with correlated timestamp
self.i2c_readings.append({ self.i2c_readings.append({
@ -146,7 +167,9 @@ class RunExecutor:
else: else:
# I2C read failed - count the failure # I2C read failed - count the failure
self.i2c_failures += 1 self.i2c_failures += 1
if callback_count[0] <= 3:
print(f"[DEBUG] I2C read failed: {status}")
# Create packet config with callback # Create packet config with callback
packet_config_with_callback = PacketConfig( packet_config_with_callback = PacketConfig(
enable=packet_config.enable, enable=packet_config.enable,
@ -155,7 +178,12 @@ class RunExecutor:
end_marker=packet_config.end_marker, end_marker=packet_config.end_marker,
on_packet_callback=on_uart_packet_detected if i2c_port else None 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 # Start listening with packet detection on LOGGER PORT
status = uart_start_listening_with_packets(uart_logger_port, packet_config_with_callback) 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") 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 # Parse hex string to bytes
command_bytes = self._parse_hex_string(command_hex) command_bytes = self._parse_hex_string(command_hex)
if not command_bytes: if not command_bytes:
if uart_logger_port: if uart_logger_port:
uart_stop_listening(uart_logger_port) uart_stop_listening(uart_logger_port)
return ("error", 0, f"Invalid command hex string: {command_hex}") return ("error", 0, f"Invalid command hex string: {command_hex}")
# Send command via COMMAND PORT # Emit TX data (command to be sent)
status, written = uart_write(uart_command_port, command_bytes) 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 status != UARTStatus.OK:
if uart_logger_port: if uart_logger_port:
uart_stop_listening(uart_logger_port) uart_stop_listening(uart_logger_port)
return ("error", 0, "Failed to send UART command") return ("error", 0, f"PGKomm2 command failed: {status}")
# Emit TX data (command sent) # Emit RX data (frames received) - only show Echo and Response, skip SB broadcasts
if raw_data_callback: if raw_data_callback and frames:
hex_tx = ' '.join(f'{b:02X}' for b in command_bytes) for frame in frames:
raw_data_callback("TX", hex_tx) if len(frame) >= 5:
adr1, adr2 = frame[2], frame[3]
# ================================================================
# 3. Wait for stop condition # Skip SB status broadcasts (background noise from device)
# ================================================================ if adr1 == 0x53 and adr2 == 0x42: # SB
continue
# Wait for timeout
time.sleep(stop_timeout_ms / 1000.0) 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
# 3.5. Handle raw data if packet detection disabled ascii_rx = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in ascii_data)
# ================================================================
if adr1 == 0x50 and adr2 == 0x48: # PH echo
if not packet_config.enable: raw_data_callback("RX", f"{hex_rx} (Echo) | '{ascii_rx}'")
# No packet detection - read raw buffer from COMMAND PORT (ACK/response) elif adr1 == 0x48 and adr2 == 0x50: # HP response
status_read, raw_data = uart_read_buffer(uart_command_port) raw_data_callback("RX", f"{hex_rx} (Response) | '{ascii_rx}'")
else:
if status_read == UARTStatus.OK and raw_data: raw_data_callback("RX", f"{hex_rx} | '{ascii_rx}'")
# Emit RX data else:
if raw_data_callback: # Unknown frame format
hex_rx = ' '.join(f'{b:02X}' for b in raw_data) hex_rx = ' '.join(f'{b:02X}' for b in frame)
raw_data_callback("RX", hex_rx) 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)
# Stop listening on logger port (if active) raw_data_callback("RX", f"{hex_rx} | '{ascii_rx}'")
if uart_logger_port:
uart_stop_listening(uart_logger_port)
# ================================================================ # ================================================================
# 4. Get detected packets (from LOGGER PORT if exists) # 3. Wait for logger packets (polling mode with stop condition)
# ================================================================ # ================================================================
uart_packets = [] 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: else:
uart_packets = [] # No logger port
packet_count = len(uart_packets) packet_count = 0
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)")
# ================================================================ # ================================================================
# 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: 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_id=session_id,
session_name=session_name, session_name=session_name,
run_no=run_no, run_no=run_no,
run_command_id=command_id, run_command_id=command_id,
packet_info=pkt, packet_info=pkt,
i2c_bytes=i2c_bytes,
run_start_ns=run_start_ns 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 # Commit database changes
self.db_conn.commit() 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 # Report errors if any via callback
if self.i2c_failures > 0 and raw_data_callback: if self.i2c_failures > 0 and raw_data_callback:
raw_data_callback("ERROR", f"I2C read failures: {self.i2c_failures}") raw_data_callback("ERROR", f"I2C read failures: {self.i2c_failures}")
@ -289,33 +408,36 @@ class RunExecutor:
except: except:
return None return None
def _save_uart_telemetry( def _save_combined_telemetry(
self, self,
session_id: str, session_id: str,
session_name: str, session_name: str,
run_no: int, run_no: int,
run_command_id: int, run_command_id: int,
packet_info: PacketInfo, packet_info: PacketInfo,
i2c_bytes: Optional[bytes],
run_start_ns: int 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. Saves to both telemetry_raw and telemetry_decoded tables.
UART and I2C data are correlated by timestamp and saved together.
""" """
# Decode packet # Decode packets
decoded = decode_uart_packet(packet_info.data) 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 # Calculate relative time from run start
time_ms = (packet_info.start_timestamp - run_start_ns) / 1_000_000.0 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 = self.db_conn.cursor()
cursor.execute(""" cursor.execute("""
INSERT INTO telemetry_raw ( INSERT INTO telemetry_raw (
session_id, session_name, run_no, run_command_id, session_id, session_name, run_no, run_command_id,
t_ns, time_ms, uart_raw_packet t_ns, time_ms, uart_raw_packet, i2c_raw_bytes
) VALUES (?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", ( """, (
session_id, session_id,
session_name, session_name,
@ -323,11 +445,12 @@ class RunExecutor:
run_command_id, run_command_id,
packet_info.start_timestamp, packet_info.start_timestamp,
time_ms, time_ms,
packet_info.data packet_info.data,
i2c_bytes # Can be None if no I2C
)) ))
# Save to telemetry_decoded (main data) # 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 # TODO: Update when decoder is fully implemented
cursor.execute(""" cursor.execute("""
INSERT INTO telemetry_decoded ( INSERT INTO telemetry_decoded (
@ -342,67 +465,10 @@ class RunExecutor:
packet_info.start_timestamp, packet_info.start_timestamp,
time_ms 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: # 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], uart_logger_port: Optional[UARTPort],
i2c_port: Optional[I2CHandle], i2c_port: Optional[I2CHandle],
packet_config: PacketConfig, packet_config: PacketConfig,
i2c_address: int = 0x40,
i2c_register: int = 0xFE,
stop_timeout_ms: int = 5000, stop_timeout_ms: int = 5000,
grace_timeout_ms: int = 1500,
raw_data_callback = None raw_data_callback = None
) -> Tuple[str, int, str]: ) -> Tuple[str, int, str]:
""" """
Execute a single RUN (convenience function). Execute a single RUN (convenience function).
Args: Args:
db_connection: Database connection db_connection: Database connection
session_id: Session ID session_id: Session ID
@ -438,8 +507,9 @@ def execute_run(
i2c_port: I2C port (optional) i2c_port: I2C port (optional)
packet_config: Packet detection configuration packet_config: Packet detection configuration
stop_timeout_ms: Stop condition timeout 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) raw_data_callback: Callback for raw data display (direction, hex_string)
Returns: Returns:
(status, packet_count, error_msg) (status, packet_count, error_msg)
""" """
@ -454,7 +524,10 @@ def execute_run(
uart_logger_port=uart_logger_port, uart_logger_port=uart_logger_port,
i2c_port=i2c_port, i2c_port=i2c_port,
packet_config=packet_config, packet_config=packet_config,
i2c_address=i2c_address,
i2c_register=i2c_register,
stop_timeout_ms=stop_timeout_ms, stop_timeout_ms=stop_timeout_ms,
grace_timeout_ms=grace_timeout_ms,
raw_data_callback=raw_data_callback raw_data_callback=raw_data_callback
) )

@ -761,7 +761,10 @@ class Session(QObject):
uart_logger_port=self.uart_logger_port, uart_logger_port=self.uart_logger_port,
i2c_port=self.i2c_handle, i2c_port=self.i2c_handle,
packet_config=self.packet_config, 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'], 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) raw_data_callback=lambda direction, hex_str: self.raw_data_received.emit(direction, hex_str)
) )

@ -529,18 +529,24 @@ class SessionWidget(QWidget):
def _on_raw_data_received(self, direction: str, hex_string: str): def _on_raw_data_received(self, direction: str, hex_string: str):
""" """
Handle raw UART data display. Handle raw UART data display.
Args: Args:
direction: "TX" or "RX" direction: "TX", "RX", or "ERROR"
hex_string: Hex bytes (e.g., "EF FE 01 02 03") hex_string: Hex bytes (e.g., "EF FE 01 02 03") or error message
""" """
if direction == "TX": if direction == "TX":
color = "green" color = "green"
prefix = "→ TX" prefix = "→ TX"
elif direction == "ERROR":
color = "red"
prefix = "✗ ERROR"
elif direction == "INFO":
color = "gray"
prefix = " INFO"
else: else:
color = "blue" color = "blue"
prefix = "← RX" prefix = "← RX"
self.log_display.append( self.log_display.append(
f"<span style='color: {color};'>[{prefix}] {hex_string}</span>" f"<span style='color: {color};'>[{prefix}] {hex_string}</span>"
) )

@ -80,8 +80,9 @@ __all__ = [
# NEW: Listening with packet detection # NEW: Listening with packet detection
'uart_start_listening_with_packets', 'uart_start_listening_with_packets',
'uart_get_detected_packets', 'uart_get_detected_packets',
'uart_get_packet_errors',
'uart_clear_detected_packets', 'uart_clear_detected_packets',
# Status # Status
'uart_get_status', 'uart_get_status',
] ]
@ -136,8 +137,8 @@ class UARTConfig:
baudrate: int baudrate: int
data_bits: int = 8 data_bits: int = 8
stop_bits: int = 1 stop_bits: int = 1
parity: str = 'N' parity: str = 'E'
buffer_size: int = 40 * 1024 * 1024 # 40MB default buffer buffer_size: int = 0.256 * 1024 * 1024 # 4MB default buffer
read_chunk_size: int = 512 read_chunk_size: int = 512
stop_mode: StopConditionMode = StopConditionMode.TIMEOUT stop_mode: StopConditionMode = StopConditionMode.TIMEOUT
@ -263,6 +264,7 @@ class UARTPort:
_packet_buffer: bytearray = field(default_factory=bytearray) _packet_buffer: bytearray = field(default_factory=bytearray)
_packet_detection_active: bool = False _packet_detection_active: bool = False
_packet_start_timestamp: float = 0.0 _packet_start_timestamp: float = 0.0
_packet_errors: int = 0 # Count of packets with end marker errors
# Timestamp function # Timestamp function
_get_timestamp: Callable[[], float] = field(default=time.perf_counter) _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: def _detect_packets_in_chunk(port: UARTPort, chunk: bytes, timestamp: float) -> None:
""" """
Detect packets in received chunk. Detect packets in received chunk.
Uses configured packet format (start marker, length, end marker). Uses configured packet format (start marker, length, end marker).
Stores complete packets in _detected_packets list with timestamps. Stores complete packets in _detected_packets list with timestamps.
Packet format: [START_MARKER][DATA][END_MARKER] Packet format: [START_MARKER][DATA][END_MARKER]
Example: EF FE [14 bytes] EE Example: EF FE [14 bytes] EE
Args: Args:
port: UART port instance port: UART port instance
chunk: Received data chunk 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: if not port._packet_config or not port._packet_config.enable:
return return
cfg = port._packet_config cfg = port._packet_config
# Add chunk to packet buffer # Add chunk to packet buffer
port._packet_buffer.extend(chunk) port._packet_buffer.extend(chunk)
# Process buffer looking for complete packets # Process buffer looking for complete packets
while len(port._packet_buffer) >= (cfg.packet_length or 0): while len(port._packet_buffer) >= (cfg.packet_length or 0):
# Look for start marker # Look for start marker
if cfg.start_marker: if cfg.start_marker:
# Find start marker position # Find start marker position
start_idx = port._packet_buffer.find(cfg.start_marker) start_idx = port._packet_buffer.find(cfg.start_marker)
if start_idx == -1: if start_idx == -1:
# No start marker found - clear old data, keep last few bytes # No start marker found - keep searching in larger buffer
# (in case start marker is split across chunks) # Only clear if buffer gets excessively large (prevent memory issues)
if len(port._packet_buffer) > 100: max_buffer_size = 1024 * 1024 # 1MB should be plenty for packet detection
port._packet_buffer = port._packet_buffer[-10:] 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 break
# Remove everything before start marker # 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:] actual_end = packet_bytes[expected_end_pos:]
if actual_end != cfg.end_marker: 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) port._packet_buffer.pop(0)
continue 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) # Timestamp at packet START (when we found start marker)
if port._packet_start_timestamp == 0.0: if port._packet_start_timestamp == 0.0:
port._packet_start_timestamp = timestamp port._packet_start_timestamp = timestamp
# Create packet info # Create packet info
with port._lock: with port._lock:
port._total_packets += 1 port._total_packets += 1
@ -562,9 +568,11 @@ def _detect_packets_in_chunk(port: UARTPort, chunk: bytes, timestamp: float) ->
if cfg.on_packet_callback: if cfg.on_packet_callback:
try: try:
cfg.on_packet_callback(packet_info.start_timestamp) cfg.on_packet_callback(packet_info.start_timestamp)
except Exception: except Exception as e:
# Don't crash reader thread if callback fails # 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 # Remove packet from buffer
port._packet_buffer = port._packet_buffer[cfg.packet_length:] 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: if cfg.on_packet_callback:
try: try:
cfg.on_packet_callback(packet_info.start_timestamp) cfg.on_packet_callback(packet_info.start_timestamp)
except Exception: except Exception as e:
# Don't crash reader thread if callback fails # 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:] port._packet_buffer = port._packet_buffer[cfg.packet_length:]
else: else:
@ -690,7 +700,8 @@ def uart_send_and_receive(port: UARTPort, tx_data: bytes,
def uart_send_and_read_pgkomm2(port: UARTPort, def uart_send_and_read_pgkomm2(port: UARTPort,
tx_data: bytes, tx_data: bytes,
capture_max_ms: int = 30, 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. 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) 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) 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) max_frames: Maximum number of frames to parse (safety limit)
log_callback: Optional callback(direction, message) for logging (e.g., to GUI)
Returns: Returns:
(Status.OK, [frame1, frame2, ...]) on success (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 # Try to extract complete frames as they arrive
while True: while True:
frame = _extract_pgkomm2_frame(rx_buffer) frame = _extract_pgkomm2_frame(rx_buffer, log_callback)
if frame is None: if frame is None:
break # Need more bytes break # Need more bytes
@ -816,15 +828,27 @@ def uart_send_and_read_pgkomm2(port: UARTPort,
return (Status.OK, collected_frames) return (Status.OK, collected_frames)
elif len(collected_frames) > 0: elif len(collected_frames) > 0:
# Got frames but no HP response (only SB broadcasts or PH echo without answer) # 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) return (Status.TIMEOUT, None)
elif len(rx_buffer) > 0: elif len(rx_buffer) > 0:
# Unparseable data - log for debugging # 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) return (Status.IO_ERROR, None)
else: else:
# No response # 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) return (Status.TIMEOUT, None)
except Exception as e: except Exception as e:
@ -843,7 +867,7 @@ def uart_send_and_read_pgkomm2(port: UARTPort,
uart_start_reader(port) 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). Extract ONE complete PGKomm2 frame from buffer (destructive).
@ -857,6 +881,7 @@ def _extract_pgkomm2_frame(buffer: bytearray) -> Optional[bytes]:
Args: Args:
buffer: Bytearray to extract from (will be modified!) buffer: Bytearray to extract from (will be modified!)
log_callback: Optional callback(direction, message) for logging
Returns: Returns:
Complete frame as bytes, or None if no complete frame available 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: if calculated_bcc != received_bcc:
# BCC mismatch - frame corrupted! # BCC mismatch - frame corrupted!
adr_str = f"{frame[2]:02X} {frame[3]:02X}" if len(frame) >= 4 else "??" 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}") error_msg = f"[PGKOMM2] ✗ BCC FAIL: ADR={adr_str}, calc={calculated_bcc:02X}, recv={received_bcc:02X}"
print(f"[PGKOMM2] Frame: {frame.hex(' ').upper()}") 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 # Drop this frame and continue searching
del buffer[:i + total] del buffer[:i + total]
return None # Reject corrupted frame 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_config = packet_config
port._packet_buffer.clear() port._packet_buffer.clear()
port._packet_start_timestamp = 0.0 port._packet_start_timestamp = 0.0
port._packet_errors = 0 # Reset error counter
# Mark listening start time # Mark listening start time
with port._lock: with port._lock:
port._listening_start_time = port._get_timestamp() port._listening_start_time = port._get_timestamp()
@ -1096,6 +1131,16 @@ def uart_get_detected_packets(port: UARTPort) -> list:
return port._detected_packets.copy() 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: def uart_clear_detected_packets(port: UARTPort) -> Status:
""" """
Clear detected packets list. Clear detected packets list.
@ -1108,10 +1153,11 @@ def uart_clear_detected_packets(port: UARTPort) -> Status:
port._detected_packets.clear() port._detected_packets.clear()
port._packet_buffer.clear() port._packet_buffer.clear()
port._packet_start_timestamp = 0.0 port._packet_start_timestamp = 0.0
port._packet_errors = 0 # Reset error counter
with port._lock: with port._lock:
port._total_packets = 0 port._total_packets = 0
return Status.OK return Status.OK

@ -35,7 +35,7 @@ from PyQt6.QtGui import QFont
# Import UART core and buffer widget # Import UART core and buffer widget
from uart.uart_kit.uart_core import * from uart.uart_kit.uart_core import *
from buffer_kit.buffer_widget_compact import CompactBufferWidget 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): class UARTCommandWorker(QThread):
@ -101,10 +101,14 @@ class UARTWidget(QWidget):
# Overflow tracking (for Data Monitor warnings) # Overflow tracking (for Data Monitor warnings)
self._last_overflow_count = 0 self._last_overflow_count = 0
# Packet detection state (NEW) # Packet detection state (NEW)
self._packet_detection_enabled = False self._packet_detection_enabled = False
self._detected_packet_count = 0 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 # Build UI
self._init_ui() self._init_ui()
@ -301,10 +305,10 @@ class UARTWidget(QWidget):
row3.addWidget(QLabel("Length:")) row3.addWidget(QLabel("Length:"))
self.spin_packet_length = QSpinBox() self.spin_packet_length = QSpinBox()
self.spin_packet_length.setRange(1, 1024) 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.setSuffix(" B")
self.spin_packet_length.setMaximumWidth(80) 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) row3.addWidget(self.spin_packet_length)
# End marker # End marker
@ -318,14 +322,14 @@ class UARTWidget(QWidget):
self.lbl_packet_count = QLabel("Packets: 0") self.lbl_packet_count = QLabel("Packets: 0")
self.lbl_packet_count.setStyleSheet("color: gray; font-weight: bold;") self.lbl_packet_count.setStyleSheet("color: gray; font-weight: bold;")
row3.addWidget(self.lbl_packet_count) row3.addWidget(self.lbl_packet_count)
# Stretch at end # Stretch at end
row3.addStretch() row3.addStretch()
# Initially disabled (only for Listening mode) # Initially disabled (only for Listening mode)
self.check_packet_detect.setEnabled(False) self.check_packet_detect.setEnabled(False)
self._on_packet_detect_toggled(0) # Disable fields self._on_packet_detect_toggled(0) # Disable fields
main_layout.addLayout(row3) main_layout.addLayout(row3)
# Update UI based on initial mode # Update UI based on initial mode
@ -416,7 +420,7 @@ class UARTWidget(QWidget):
# Listening mode update timer # Listening mode update timer
self.listen_timer = QTimer() self.listen_timer = QTimer()
self.listen_timer.timeout.connect(self._update_listening_display) self.listen_timer.timeout.connect(self._update_listening_display)
# Polling mode timer # Polling mode timer
self.poll_timer = QTimer() self.poll_timer = QTimer()
self.poll_timer.timeout.connect(self._poll_next_packet) self.poll_timer.timeout.connect(self._poll_next_packet)
@ -515,7 +519,7 @@ class UARTWidget(QWidget):
data_bits=int(self.combo_databits.currentText()), data_bits=int(self.combo_databits.currentText()),
stop_bits=int(self.combo_stopbits.currentText()), stop_bits=int(self.combo_stopbits.currentText()),
parity='N' if self.combo_parity.currentText() == 'None' else self.combo_parity.currentText()[0], 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_mode=stop_mode,
stop_timeout_ms=self.spin_timeout.value(), stop_timeout_ms=self.spin_timeout.value(),
stop_terminator=terminator, stop_terminator=terminator,
@ -603,6 +607,10 @@ class UARTWidget(QWidget):
# Start listening with packet detection # Start listening with packet detection
uart_start_listening_with_packets(self.port, packet_config) uart_start_listening_with_packets(self.port, packet_config)
self._packet_detection_enabled = True 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.poll_timer.start(50) # Poll for packets
self._log_info(f"Polling mode started with packet detection") 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'}") 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: except Exception as e:
self._show_error(f"Invalid packet config: {e}") self._show_error(f"Invalid packet config: {e}")
# Fallback to regular polling # 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.poll_timer.start(50)
self._packet_detection_enabled = False self._packet_detection_enabled = False
self._log_warning("Fallback to polling mode without packet detection") self._log_warning("Fallback to polling mode without packet detection")
else: else:
# Regular polling mode (use uart_poll_packet) # 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.poll_timer.start(50)
self._packet_detection_enabled = False self._packet_detection_enabled = False
self._log_info("Polling mode started (no packet detection)") 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).""" """Poll for next packet in polling mode (NO printing to monitor)."""
if not self.port: if not self.port:
return 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 # Update buffer widget
if isinstance(self.buffer_widget, CompactBufferWidget): if isinstance(self.buffer_widget, CompactBufferWidget):
self.buffer_widget.update_display() self.buffer_widget.update_display()
# If packet detection is enabled, update packet count (NO printing) # If packet detection is enabled, update packet count (NO printing)
if self._packet_detection_enabled: if self._packet_detection_enabled:
packets = uart_get_detected_packets(self.port) packets = uart_get_detected_packets(self.port)
current_count = len(packets) current_count = len(packets)
if current_count != self._detected_packet_count: 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._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.setText(f"Packets: {current_count}")
self.lbl_packet_count.setStyleSheet("color: green; font-weight: bold;") self.lbl_packet_count.setStyleSheet("color: green; font-weight: bold;")
return return
@ -853,8 +896,11 @@ class UARTWidget(QWidget):
# Poll for packet (non-blocking) # Poll for packet (non-blocking)
status, packet = uart_poll_packet(self.port) status, packet = uart_poll_packet(self.port)
if status == Status.OK: if status == Status.OK:
# Update packet timestamp
self._last_packet_time = current_time
# Display stop condition message in gray (logging mode only) # Display stop condition message in gray (logging mode only)
stop_msg = ( stop_msg = (
f"<span style='color: gray;'>" f"<span style='color: gray;'>"
@ -865,10 +911,10 @@ class UARTWidget(QWidget):
f"</span>" f"</span>"
) )
self.text_display.append(stop_msg) self.text_display.append(stop_msg)
# Update buffer widget # Update buffer widget
self.buffer_widget.update_display() self.buffer_widget.update_display()
# Emit packet signal # Emit packet signal
self.packet_received.emit(packet) self.packet_received.emit(packet)

Loading…
Cancel
Save