From 70eb58531f9388e7289f25e26d25aa27b8cf7f08 Mon Sep 17 00:00:00 2001 From: Kerem Date: Fri, 21 Nov 2025 17:58:17 +0100 Subject: [PATCH] Telemetry is wokring now on how to deocde it --- buffer_kit/buffer_widget_compact.py | 34 ++- database/ehinge.db | Bin 106496 -> 106496 bytes run.py | 369 +++++++++++++++++----------- session.py | 3 + session_widget.py | 14 +- uart/uart_kit/uart_core.py | 108 +++++--- uart/uart_kit/uart_core_widget.py | 78 ++++-- 7 files changed, 400 insertions(+), 206 deletions(-) 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 a4c1f84ce4418e61ee5899f22abfbd3a4711c6db..6dfdc40af64eddfe5dcabcee1561c48121fe414c 100644 GIT binary patch delta 640 zcmZWnO=uHA6n?YQ#B5CRjO0hD4Y?HYkY;ALsV0|FWdnsCgz80+m?qn>CG5uRHdLjU zttTPDl!EI?idRn|mmH%g7%r})0}=)SpJ1@>{)#*^r4%GV1(>BoMrm@%NfO0)oXAyuZL_8@C70nmqwq9e zC@41yD$iE+R@QMI-(9`Q$Oi~$@ahD_>9;YY71Kp6qG_CM%`>)D)FhXZ6wH9C{_V8) z!g?wM9ReQWk3qEY6%#T|l5tn*Je`qkr)*ZsO=m#b{tE+t@8=PQM=%xI09+1jgbyO` z`tH5Z<(*YY2KD~)vZ@9Q({M^co*}H)y=xtE?0xExsKl9fu1i>CY=Y+txx9$&((Y0$ z<=&=(o!cRT{b+N{F8!dmjZhM*b=W{;G|Q1^kn% z^1FcCFAV%&HZwN7;^*OJmgOuiNz6-0EGhwtunBBtoACQRKVKCC3y&ZJzbB6%?*U%N zjg5>v5>2{HoD7E1&Em$!9G-cZCC>S|xj>CR)0=x3KTMy|%P1#mpkQcXWol?;WLe9? z$)L#FXl=~EF@1j@qZyOA!SsS&Mv>_}{fzvEMg|78v9(49My9%khPp;zZN`>XhQ^KN z%$yABrcI1Mea<=g#i#FBdDFl3GA?2P`tJP1#?#Y1`x$re134`GI~e$n^WWtE zz`tX&V8IH04?bo^#{7cRy!d30W)?wabw*@kQklh=4H=OgpP!zdnU~JQ&#cNBl3H8> G3rzrXlyrXp 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)