import threading import time import socket import ctypes import queue import struct import os import logging from dataclasses import dataclass, field from typing import Dict, Type, Optional, Callable, List from types import SimpleNamespace from .monitor import MonDecoded, MonitorInfo, MonitorBuffer from ..lib1553.message_base import BaseMessage from ..lib1553.messages.a1_settings import MsgA1Payload from ..lib1553.messages.a2_operation_command import MsgA2Payload from ..lib1553.messages.a3_graphic_setting import MsgA3Payload from ..lib1553.messages.a4_nav_data_and_cursor import MsgA4Payload from ..lib1553.messages.a7_data_link_targets_1 import MsgA7Payload from ..lib1553.messages.a8_data_link_targets_2 import MsgA8Payload from ..lib1553.messages.inu_high_speed import MsgInuHighSpeedPayload from ..lib1553.messages.b2_tws_targets_3_4_5 import MsgB2Payload from ..lib1553.messages.b3_tws_targets_6_7_8 import MsgB3Payload from ..lib1553.messages.b4_spt_target import MsgB4Payload from ..lib1553.messages.b8_bit_report import MsgB8Payload from ..lib1553.messages.tws_status_and_targets import TwsStatusAndTargets0102 from ..lib1553.messages.tracked_target import TrackedTarget01 from ..lib1553.messages.msg_rdr_settings_and_parameters_tellback import MsgRdrSettingsAndParametersTellback from ..lib1553.messages.msg_rdr_status_tellback import MsgRdrStatusTellback from ..lib1553.structures import Udp1553Header, Udp1553Message, CommandWord from ..lib1553.constants import Marker from .introspection import structure_to_dict # MonDecoded and MonitorInfo are provided by pymsc.core.monitor class AppController: def __init__(self): self._running = threading.Event() self._lock = threading.Lock() # Debug flag: when True, receiver prints raw packets and sender addr self.debug = True self.messages: Dict[str, BaseMessage] = {} self.rx_map: Dict[int, Type[ctypes.Structure]] = {} self.monitor_queue = queue.Queue() # Per-SA statistics for Bus Monitor summary # Keyed by subaddress int -> dict: {name, sa, count, errs, last_sw, last_err, last_time, period_ms, wc, rt} self.stats: Dict[int, dict] = {} self._scheduler_thread = None self._receiver_thread = None # Monitor buffer (replica comportamento C++ AvbDriverUDP) # Use the centralized MonitorBuffer class in pymsc.core.monitor self.monitor = MonitorBuffer(buf_size=512) # Flag to control whether periodic transmissions are actually sent. # Mirrors C++ behaviour where the driver only starts on a explicit `go()`. self._sending_enabled = False # MODIFICA 1: Bind su 0.0.0.0 per ascoltare da qualsiasi interfaccia di rete # Se il radar invia dati alla tua macchina, li riceverai indipendentemente dall'IP. self.udp_ip = "0.0.0.0" self.udp_send_port = 51553 # Porta per INVIARE comandi al radar self.udp_recv_port = 61553 # Porta per RICEVERE dati dal radar (bus monitor) self._sock = None # Flag per controllare se il radar è stato inizializzato self.radar_initialized = False self.radar_dest_ip = "127.0.0.1" # IP destinazione radar self._init_messages() # Percorsi dei file di log try: repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) except Exception: repo_root = os.getcwd() self.logs_dir = os.path.join(repo_root, 'logs') self.debug_log_path = os.path.join(self.logs_dir, 'app_debug.log') self.rx_log_path = os.path.join(self.logs_dir, 'rx_messages.log') self.tx_log_path = os.path.join(self.logs_dir, 'tx_messages.log') self.raw_log_path = os.path.join(self.logs_dir, 'udp_raw.log') # Frame counter per UDP1553Header (incrementato ad ogni frame inviato) self.frame_counter = 0 # Diagnostic dumps limit to avoid flooding logs during normal operation self._diag_dump_remaining = 50 self._diag_log_path = os.path.join(self.logs_dir, 'diag_B_parsing.log') # Temporary raw packet dump budget for debugging reception (disabled by default) self._raw_dump_remaining = 20 self._raw_log_path = os.path.join(self.logs_dir, 'udp_raw.log') def _setup_logging(self): """ Configura il sistema di logging con file azzerati ad ogni avvio. Crea 3 file di log nella cartella logs/: - app_debug.log: Log generale dell'applicazione - rx_messages.log: Messaggi ricevuti dal radar - tx_messages.log: Messaggi inviati al radar """ try: os.makedirs(self.logs_dir, exist_ok=True) # Azzera i file di log ad ogni avvio (mode='w') with open(self.debug_log_path, 'w', encoding='utf-8') as f: f.write(f"=== PyMsc Debug Log - Started {time.strftime('%Y-%m-%d %H:%M:%S')} ===\n") with open(self.rx_log_path, 'w', encoding='utf-8') as f: f.write(f"=== RX Messages Log - Started {time.strftime('%Y-%m-%d %H:%M:%S')} ===\n") with open(self.tx_log_path, 'w', encoding='utf-8') as f: f.write(f"=== TX Messages Log - Started {time.strftime('%Y-%m-%d %H:%M:%S')} ===\n") with open(self.raw_log_path, 'w', encoding='utf-8') as f: f.write(f"=== UDP Raw Packets Log - Started {time.strftime('%Y-%m-%d %H:%M:%S')} ===\n") print(f"Core: Logging initialized in {self.logs_dir}") self._log_debug("Application started") except Exception as e: print(f"Warning: Could not setup logging: {e}") def _diag_dump(self, text: str): """Write a diagnostic line to the diag log and decrement remaining counter.""" try: if getattr(self, '_diag_dump_remaining', 0) <= 0: return # ensure logs dir exists os.makedirs(self.logs_dir, exist_ok=True) with open(self._diag_log_path, 'a', encoding='utf-8') as f: f.write(f"[{time.strftime('%H:%M:%S')}] {text}\n") # also print to console for immediate feedback print(f"[DIAG] {text}") self._diag_dump_remaining -= 1 except Exception: pass def _log_debug(self, message): """Scrive nel log di debug.""" try: with open(self.debug_log_path, 'a', encoding='utf-8') as f: f.write(f"[{time.strftime('%H:%M:%S.%f')[:-3]}] {message}\n") except Exception: pass def _log_rx(self, message): """Scrive nel log dei messaggi ricevuti.""" try: with open(self.rx_log_path, 'a', encoding='utf-8') as f: f.write(f"[{time.strftime('%H:%M:%S.%f')[:-3]}] {message}\n") except Exception: pass def _log_tx(self, message): """Scrive nel log dei messaggi trasmessi.""" try: with open(self.tx_log_path, 'a', encoding='utf-8') as f: f.write(f"[{time.strftime('%H:%M:%S.%f')[:-3]}] {message}\n") except Exception: pass def register_message(self, message: BaseMessage): with self._lock: self.messages[message.label] = message # Registra la mappatura per la ricezione (usa il SubAddress del CW) sa = message.cw.fields.sa self.rx_map[sa] = message.payload.__class__ # --- Bus Monitor API (replicate dal driver C++) def set_mon_isr(self, callback: Callable[[object], None]): """Register a callback to be invoked when monitor buffer reaches threshold. The callback will be invoked with a snapshot-like object containing `buffer` (list of MonDecoded), `total_message` and `processed_message`. Invocation is performed in a daemon thread to avoid blocking receiver. """ # Delegate to MonitorBuffer self.monitor.set_isr(callback) def enableMonitorIsr(self, enable: bool = True): """Enable or disable the monitor ISR callback invocation.""" self.monitor.enable_isr(enable) def getMonitorRawBuffer(self): """Return a snapshot of the current monitor buffer and reset internal storage. Returns an object with attributes: `buffer` (list of MonDecoded), `total_message` (int) and `processed_message` (int). """ return self.monitor.get_raw_buffer() # --- Control methods to mirror legacy driver start/stop behaviour def go(self, enable_monitor: bool = True): """Enable periodic sending of messages and (optionally) the monitor ISR. This mirrors the C++ `avbDriver->go()` behaviour: messages are configured by `initialize_radar()` but actual periodic transmission starts only when `go()` is invoked. """ self._sending_enabled = True if enable_monitor: try: self.enableMonitorIsr(True) except Exception: pass self._log_debug("Core: go() invoked - sending enabled") return True def stop_sending(self): """Disable periodic sending and (optionally) the monitor ISR.""" self._sending_enabled = False try: self.enableMonitorIsr(False) except Exception: pass self._log_debug("Core: stop_sending() invoked - sending disabled") return True def go_mon(self): """Enable only the monitor ISR (replicates avbDriver->go_mon).""" return self.enableMonitorIsr(True) def stop_mon(self): """Disable only the monitor ISR.""" return self.enableMonitorIsr(False) def start(self): print(f"Core: Starting services (Listening on {self.udp_ip}:{self.udp_recv_port})...") self._running.set() # Setup logging con file azzerato ad ogni avvio self._setup_logging() self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: self._sock.bind((self.udp_ip, self.udp_recv_port)) except OSError as e: print(f"Error binding socket: {e}") self._scheduler_thread = threading.Thread(target=self._scheduler_loop, daemon=True) self._scheduler_thread.start() self._receiver_thread = threading.Thread(target=self._receiver_loop, daemon=True) self._receiver_thread.start() def stop(self): print("Core: Stopping services...") self._running.clear() if self._sock: self._sock.close() if self._scheduler_thread: self._scheduler_thread.join(timeout=1.0) if self._receiver_thread: self._receiver_thread.join(timeout=1.0) def initialize_radar(self): """ Invia messaggi di inizializzazione al radar. Replica il comportamento di test_1553.py per configurare il radar. """ print("Core: Initializing radar...") self._log_debug("=== RADAR INITIALIZATION STARTED ===") # Importa gli enum necessari from ..lib1553.datatypes.enums import TargetHistory, RdrModes try: # A1 - Settings and Parameters a1 = self.messages.get('A1') if a1: # Imposta valori di default come in test_1553.py # Nota: nel nuovo codice l'enum è LEVEL_04 invece di TARGET_HISTORY_LEVEL_04 a1.payload.settings.bits.target_history = TargetHistory.LEVEL_04 a1.payload.settings.bits.symbology_intensity = 127 self._send_single_message(a1) self._log_tx("INIT: A1 (Settings) sent - history=LEVEL_04, intensity=127") time.sleep(0.05) print(" → A1 (Settings) sent") # A2 - Operation Command a2 = self.messages.get('A2') if a2: # Imposta master mode RWS come in test_1553.py a2.payload.rdr_mode_command.bits.master_mode = RdrModes.RWS # Imposta param1 e param2 come in test_1553.py if hasattr(a2.payload, 'param1'): a2.payload.param1.raw = 0 # set_gm_submode(0) if hasattr(a2.payload, 'param2'): a2.payload.param2.raw = 0 # set_spare_0_4(0) self._send_single_message(a2) time.sleep(0.05) print(" → A2 (Operation Command) sent") # A3 - Graphic Setting a3 = self.messages.get('A3') if a3: self._send_single_message(a3) time.sleep(0.05) print(" → A3 (Graphic Setting) sent") # A4 - Nav Data and Cursor a4 = self.messages.get('A4') if a4: a4.payload.validity_and_slew.raw = 0 self._send_single_message(a4) time.sleep(0.05) print(" → A4 (Nav Data) sent") # A5 - INU High Speed a5 = self.messages.get('A5') if a5: a5.payload.timetag.raw = 0 self._send_single_message(a5) time.sleep(0.05) print(" → A5 (INU High Speed) sent") # IMPORTANTE: Attiva invio periodico per mantenere la connessione # Imposta le frequenze come nel vecchio test_1553.py print("Core: Enabling periodic transmission...") self._log_debug("Enabling periodic message transmission...") self._enable_periodic_transmission() self.radar_initialized = True self._log_debug("=== RADAR INITIALIZATION COMPLETE ===") print("Core: Radar initialization complete!") return True except Exception as e: print(f"Core: Error initializing radar: {e}") self._log_debug(f"ERROR during radar initialization: {e}") import traceback traceback.print_exc() return False def _send_single_message(self, message: BaseMessage): """ Invia un singolo messaggio al radar. """ frame = self._prepare_frame([message]) self._send_udp(frame, self.radar_dest_ip) def _enable_periodic_transmission(self): """ Attiva la trasmissione periodica dei messaggi per mantenere la connessione con il radar. Imposta le frequenze come nel vecchio test_1553.py: - A1: 10 Hz (ogni 100ms) - A2: 25 Hz (ogni 40ms) - A3: 10 Hz (ogni 100ms) - A4: 50 Hz (ogni 20ms) - A5: 50 Hz (ogni 20ms) """ with self._lock: # Imposta i periodi in millisecondi if 'A1' in self.messages: self.messages['A1'].period_ms = 100 # 10 Hz print(" → A1: 10 Hz (100ms)") if 'A2' in self.messages: self.messages['A2'].period_ms = 40 # 25 Hz print(" → A2: 25 Hz (40ms)") if 'A3' in self.messages: self.messages['A3'].period_ms = 100 # 10 Hz print(" → A3: 10 Hz (100ms)") if 'A4' in self.messages: self.messages['A4'].period_ms = 20 # 50 Hz print(" → A4: 50 Hz (20ms)") if 'A5' in self.messages: self.messages['A5'].period_ms = 20 # 50 Hz print(" → A5: 50 Hz (20ms)") def _scheduler_loop(self): """ Invia messaggi periodici al radar usando la stessa logica del vecchio test_1553.py: - Mantiene un contatore manuale di tempo in ms - Controlla se mytime % period == 0 per decidere quando inviare - Sleep di 20ms per coerenza temporale """ print("Core: Scheduler started.") mytime = 0 while self._running.is_set(): with self._lock: active_msgs = list(self.messages.values()) # Raggruppa i messaggi che devono essere inviati in questo tick due_msgs = [] for msg in active_msgs: if msg.period_ms > 0: # Stesso check del vecchio codice: mytime % period == 0 if mytime % msg.period_ms == 0: due_msgs.append(msg) if due_msgs and self._sending_enabled: # Aggiorna A5 timetag se presente (come nel vecchio codice) a5_msg = next((m for m in due_msgs if m.label == 'A5'), None) if a5_msg: a5_msg.payload.timetag.raw += 312 # Costruiamo un unico frame UDP che contiene tutti i messaggi frame = self._prepare_frame(due_msgs) # RIMOSSO: logging su file ogni 20ms causa ritardi gravi # msg_labels = ", ".join([msg.label for msg in due_msgs]) # self._log_tx(f"Sending frame with messages: {msg_labels}") # Usa radar_dest_ip invece di hardcoded 127.0.0.1 self._send_udp(frame, self.radar_dest_ip) else: # If there are due messages but sending is disabled, skip sending. # This mimics the legacy driver where `go()` must be called to start. if due_msgs and self.debug: pass time.sleep(0.02) # 20ms come nel vecchio codice mytime += 20 # Incrementa il contatore manuale def _receiver_loop(self): """ Riceve pacchetti, decodifica header e payload. Gestisce anche messaggi sconosciuti (RAW). """ print("Core: Receiver started.") # Dimensioni fisse degli header definiti in structures.py header_size = ctypes.sizeof(Udp1553Header) wrapper_size = ctypes.sizeof(Udp1553Message) if self.debug: print(f"Core: Header size={header_size}, Wrapper size={wrapper_size}") # precompute marker bytes for recovery scanning ctrl_begin_bytes = struct.pack(" 0: os.makedirs(self.logs_dir, exist_ok=True) with open(self._raw_log_path, 'a', encoding='utf-8') as rf: rf.write(f"[{time.strftime('%H:%M:%S')}] {addr[0]}:{addr[1]} {len(data)} bytes {data[:64].hex()}\n") # Also push a short monitor entry so GUI shows we've received something try: self.monitor_queue.put((time.strftime("%H:%M:%S"), "RAW_RX", f"{addr[0]}:{addr[1]}", f"{len(data)} bytes")) except Exception: pass self._raw_dump_remaining -= 1 except Exception: pass # DEBUG: Salva pacchetti con SA >= 11 (messaggi B) in un file if len(data) > 64: # Cerca rapidamente se ci sono SA >= 11 nel pacchetto for i in range(64, len(data)-4): if data[i:i+2] == b'\x3c\x3c': # CTRL_BEGIN cw = struct.unpack_from('> 5) & 0x1F if sa >= 11: # Messaggio B trovato! # Salva il pacchetto per analisi debug_path = os.path.join(self.logs_dir, f'packet_with_B_SA{sa}.bin') with open(debug_path, 'wb') as f: f.write(data) print(f"[DEBUG] Packet with B message (SA={sa}) saved to {debug_path}") break # RIMOSSO: debug print e file I/O ogni pacchetto causa ritardi gravi # if self.debug: # print(f"[RX] {len(data)} bytes from {addr}") # self._log_rx(f"Received {len(data)} bytes from {addr[0]}:{addr[1]}") # with open(self.raw_log_path, 'a', encoding='utf-8') as f: # f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {addr[0]}:{addr[1]} {data.hex()}\n") # --- MODIFICA 2: Gestione RAW robusta --- # 1. Validazione minima lunghezza if len(data) < header_size + wrapper_size: # Pacchetto troppo piccolo o non conforme al protocollo custom self.monitor_queue.put((time.strftime("%H:%M:%S"), "INVALID LEN", "RX", data.hex())) continue # Parsing Header (Opzionale: possiamo leggere flag, errori, etc) # udp_header = Udp1553Header.from_buffer_copy(data[:header_size]) # 2. Parsing di eventuali messaggi concatenati nel frame offset = header_size timestamp = time.strftime("%H:%M:%S") # Helper: cerca il prossimo marker (CTRL_BEGIN/CTRL_END/END_1553) def find_next_marker(buf, start=0): found = -1 for marker in (ctrl_begin_bytes, ctrl_end_bytes, end_marker_bytes): pos = buf.find(marker, start) if pos != -1 and (found == -1 or pos < found): found = pos return found # Loop finché abbiamo almeno un wrapper disponibile. # Support both full wrappers (sizeof(Udp1553Message)) and # the short 12-byte variant observed in the capture. minimal_wrapper_len = 12 # Marker-first parsing: scan the datagram for CTRL_BEGIN markers and # attempt to parse either the full Udp1553Message (when available) # or the observed short 12-byte wrapper. Accept a short wrapper # only when the inverted-CW field matches the bitwise inverse of CW. search_offset = offset while True: # Find next CTRL_BEGIN marker next_begin = data.find(ctrl_begin_bytes, search_offset) if next_begin == -1: break offset = next_begin # GENERAL DIAGNOSTIC: if enabled, log marker details (CW raw, expected inverted, positions) try: if getattr(self, '_diag_dump_remaining', 0) > 0: # attempt to read cw_raw and compute related offsets without changing parsing state try: cw_probe = struct.unpack_from('> 10) & 0x1 payload_len_probe = wc_probe * 2 inverted_probe_pos = offset + 8 + payload_len_probe inverted_probe = None ctrl_end_probe = None if inverted_probe_pos + 2 <= len(data): inverted_probe = struct.unpack_from('>5)&0x1F} tr={tr_probe} wc={wc_probe} payload_len={payload_len_probe} " f"inverted=0x{inverted_probe:04X}" if inverted_probe is not None else f"MARKER offset={offset} addr={addr[0]}:{addr[1]} CW=0x{cw_probe:04X} sa={(cw_probe>>5)&0x1F} tr={tr_probe} wc={wc_probe} payload_len={payload_len_probe} inverted=None") # We prefer a single-line with expected too info2 = f" expected=0x{expected_probe:04X} inv_pos={inverted_probe_pos} ctrl_end={ctrl_end_probe}" # write two parts to keep formatting safe self._diag_dump(info + info2) except Exception: # ignore probe failures pass except Exception: pass # Prefer detecting the observed 12-byte short wrapper first short_parsed = False # Initialize variables that will be used later sa = 0 tr = 0 rt = 0 payload_bytes = b'' payload_len = 0 msg_wrapper = None if offset + minimal_wrapper_len <= len(data): try: cw_raw = struct.unpack_from('> 5) & 0x1F if sa_check >= 11: print(f"[PARSER] Offset={offset}, SA={sa_check}, CW=0x{cw_raw:04X}, inverted=0x{inverted_cw:04X}, expected=0x{expected_inverted:04X}, ctrl_end=0x{ctrl_end:04X}, match={inverted_cw == expected_inverted}") if getattr(self, '_diag_dump_remaining', 0) > 0: info = (f"SHORT offset={offset} addr={addr[0]}:{addr[1]} SA={sa_check} " f"CW=0x{cw_raw:04X} inverted=0x{inverted_cw:04X} expected=0x{expected_inverted:04X} " f"inverted_pos={inverted_cw_pos} payload_len={payload_len}") self._diag_dump(info) if inverted_cw == expected_inverted: # Valid short wrapper -> build minimal wrapper-like object # Create a simple object to hold CW, SW, error_code sa_check = (cw_raw >> 5) & 0x1F if sa_check >= 11: print(f"[INSIDE_IF] SA={sa_check}, about to create SimpleNamespace") msg_wrapper = SimpleNamespace() if sa_check >= 11: print(f"[AFTER_NS] Created SimpleNamespace, creating CommandWord") cw_obj = CommandWord() if sa_check >= 11: print(f"[AFTER_CW] Created CommandWord, setting raw={cw_raw:04X}") cw_obj.raw = cw_raw if sa_check >= 11: print(f"[AFTER_RAW] Set cw_obj.raw successfully") msg_wrapper.cw = cw_obj if sa_check >= 11: print(f"[AFTER_ASSIGN] Assigned cw to msg_wrapper") msg_wrapper.sw = struct.unpack_from(' 0: payload_bytes = data[offset+8:offset+8+payload_len] else: payload_bytes = b'' if sa_check >= 11: print(f"[BEFORE_FIELDS] About to access msg_wrapper.cw.fields") sa = msg_wrapper.cw.fields.sa if sa_check >= 11: print(f"[GOT_SA] sa={sa}") tr = msg_wrapper.cw.fields.tr rt = msg_wrapper.cw.fields.rt # DEBUG: Verifica che arriviamo qui if sa >= 11: print(f"[SHORT_PARSED] SA={sa}, offset advanced to {offset + 8 + payload_len + 4}") # Advance: 8 (wrapper) + payload + 2 (inverted) + 2 (ctrl_end) offset = offset + 8 + payload_len + 4 short_parsed = True if not short_parsed: # Fall back to attempting full-wrapper parse if offset + wrapper_size <= len(data): try: msg_wrapper = Udp1553Message.from_buffer_copy(data[offset:offset+wrapper_size]) except Exception: # malformed full wrapper -> skip this marker search_offset = offset + 1 continue # extract CW-derived payload length try: wc_field = msg_wrapper.cw.fields.wc # Treat WC=0 as 0 words for consistency with short-wrapper parsing # (captured packets use WC=0 -> no payload) word_count = int(wc_field) payload_len = word_count * 2 except Exception: payload_len = 0 # compute expected end positions expected_end = offset + wrapper_size + payload_len + 4 if expected_end <= len(data): # read payload + inverted + footer payload_bytes = data[offset+wrapper_size: offset+wrapper_size+payload_len] inverted_cw = struct.unpack_from(' report and continue searching after this marker try: sa_full = getattr(msg_wrapper.cw.fields, 'sa', 0) except Exception: sa_full = 0 if sa_full >= 11 and getattr(self, '_diag_dump_remaining', 0) > 0: info = (f"FULL_BAD offset={offset} addr={addr[0]}:{addr[1]} SA={sa_full} " f"CW=0x{int(msg_wrapper.cw.raw):04X} inverted=0x{inverted_cw:04X} expected=0x{expected_inverted:04X} " f"payload_len={payload_len} ctrl_end=0x{ctrl_end:04X}") self._diag_dump(info) self.monitor_queue.put((timestamp, f"A{getattr(msg_wrapper.cw.fields, 'sa', 0)}", "BC->RT", f"Bad inverted CW (0x{inverted_cw:04X} != 0x{expected_inverted:04X})")) search_offset = offset continue if ctrl_end != Marker.CTRL_END: self.monitor_queue.put((timestamp, f"A{getattr(msg_wrapper.cw.fields, 'sa', 0)}", "BC->RT", "Malformed footer")) search_offset = offset continue # At this point treat as a valid full wrapper message and fall through sa = msg_wrapper.cw.fields.sa if hasattr(msg_wrapper.cw, 'fields') else 0 tr = msg_wrapper.cw.fields.tr if hasattr(msg_wrapper.cw, 'fields') else 0 rt = msg_wrapper.cw.fields.rt if hasattr(msg_wrapper.cw, 'fields') else 0 word_count = word_count else: # Not enough bytes for either short or full wrapper: advance search search_offset = offset + 1 continue # At this point we have: msg_wrapper, sa, tr, rt, payload_bytes, payload_len, and offset advanced # Update search_offset to continue from current offset search_offset = offset # DEBUG: Stampa messaggi B (telemetria dal radar) if sa >= 11 and sa <= 28: print(f"[B MESSAGE] SA={sa} (B{sa-10}), TR={tr}, RT={rt}, payload_len={payload_len}, direction={'RT->BC' if tr==1 else 'BC->RT'}") # Build human readable descriptors and push to monitor as before try: sw_hex = f"0x{int(getattr(msg_wrapper, 'sw', 0)):04X}" except Exception: sw_hex = "0x0000" try: err_hex = f"0x{int(getattr(msg_wrapper, 'error_code', 0)):04X}" except Exception: err_hex = "0x0000" hex_str = payload_bytes.hex().upper() spaced = " ".join(hex_str[i:i+2] for i in range(0, len(hex_str), 2)) short = spaced if len(spaced) <= 120 else spaced[:120] + "..." # Determine decoded note if possible decoded_note = "" if sa in self.rx_map: payload_cls = self.rx_map[sa] struct_size = ctypes.sizeof(payload_cls) if struct_size == payload_len and payload_len > 0: decoded_note = f"Decoded ({struct_size} bytes)" try: payload_obj = payload_cls.from_buffer_copy(payload_bytes) payload_dict = structure_to_dict(payload_obj) import json pretty = json.dumps(payload_dict, ensure_ascii=False) if len(pretty) > 200: pretty = pretty[:200] + '...' except Exception as e: pretty = f"" else: decoded_note = f"Payload ({payload_len} bytes)" # Generate correct label: A1..A8 for SA 1..8, B1..B18 for SA 11..28 if sa in self.rx_map: if 1 <= sa <= 8: label = f"A{sa}" elif 11 <= sa <= 28: label = f"B{sa-10}" else: label = f"UNKNOWN (SA={sa})" else: label = f"UNKNOWN (SA={sa})" dir_str = "RT->BC" if tr == 1 else "BC->RT" desc = f"CW(sa={sa},rt={rt},wc={payload_len//2}) SW={sw_hex} ERR={err_hex} {decoded_note}" # Diagnostic: log when we enqueue B messages to monitor_queue try: if 11 <= sa <= 28 and getattr(self, '_diag_dump_remaining', 0) > 0: self._diag_dump(f"ENQUEUED label={('B'+str(sa-10))} dir={dir_str} desc={desc}") except Exception: pass if sa in self.rx_map and payload_len > 0 and ctypes.sizeof(self.rx_map[sa]) == payload_len and 'pretty' in locals(): self.monitor_queue.put((timestamp, label, dir_str, pretty)) # RIMOSSO: debug print e file I/O causa ritardi # if self.debug: # print(f"[QUEUE] Added {label} (decoded) to monitor_queue") # self._log_rx(f"{label} {dir_str} - Decoded payload: {pretty[:100]}") else: self.monitor_queue.put((timestamp, label, dir_str, desc)) # RIMOSSO: debug print e file I/O causa ritardi # if self.debug: # print(f"[QUEUE] Added {label} (raw) to monitor_queue: {desc[:50]}") # self._log_rx(f"{label} {dir_str} - {desc}") self.monitor_queue.put((timestamp, f"FROM {addr[0]}:{addr[1]}", dir_str, short)) # RIMOSSO: file I/O causa ritardi # self._log_rx(f" Payload hex: {short[:200]}") # --- Popola il buffer monitor strutturato (replica C++ mon.msg) try: mon_entry = MonDecoded( cw_raw=int(getattr(msg_wrapper.cw, 'raw', 0)), sw=int(getattr(msg_wrapper, 'sw', 0)), error_code=int(getattr(msg_wrapper, 'error_code', 0)), sa=int(sa), tr=int(tr), rt=int(rt), wc=int(payload_len//2), data=payload_bytes, timetag=time.time(), ) # Delegate buffer handling to MonitorBuffer try: self.monitor.append(mon_entry) except Exception: # Do not block receiver on monitor buffer errors pass except Exception: # Non blocchiamo il receiver per errori nel buffer construction pass # Update stats now = time.time() stat = self.stats.get(sa, { "name": label, "sa": sa, "count": 0, "errs": 0, "last_sw": None, "last_err": None, "last_time": None, "period_ms": None, "period_samples": [], # Rolling window of last N intervals "wc": payload_len//2, "rt": rt, }) stat["count"] += 1 try: stat["last_sw"] = int(getattr(msg_wrapper, 'sw', 0)) except Exception: stat["last_sw"] = None stat["last_err"] = int(getattr(msg_wrapper, 'error_code', 0)) if stat["last_time"] is not None: dt = (now - stat["last_time"]) * 1000.0 # Only update period if interval is significant (> 5ms) to filter duplicates in same datagram if dt > 5.0: # Add to rolling window (keep last 10 samples) samples = stat.get("period_samples", []) samples.append(dt) if len(samples) > 10: samples.pop(0) stat["period_samples"] = samples # Calculate average period from samples stat["period_ms"] = sum(samples) / len(samples) stat["last_time"] = now stat["wc"] = payload_len//2 stat["rt"] = rt self.stats[sa] = stat # Diagnostic: note that we updated stats for this SA (help GUI debugging) try: if 11 <= sa <= 28: # write unbounded short entry to rx_messages.log to confirm stats update try: with open(self.rx_log_path, 'a', encoding='utf-8') as rf: rf.write(f"[{time.strftime('%H:%M:%S')}] STATS_UPDATED SA={sa} name={stat.get('name')} count={stat.get('count')}\n") except Exception: pass if getattr(self, '_diag_dump_remaining', 0) > 0: self._diag_dump(f"STATS_UPDATED SA={sa} name={stat.get('name')} count={stat.get('count')}") except Exception: pass except socket.timeout: continue except OSError: if self._running.is_set(): print("Socket error in receiver.") break def _send_udp(self, data: bytes, dest_ip: str): if self._sock: try: self._sock.sendto(data, (dest_ip, self.udp_send_port)) except OSError as e: print(f"Send error: {e}") def _prepare_frame(self, messages: list[BaseMessage]) -> bytes: """ Prepara un datagram UDP che replica il comportamento del vecchio codice: UDP Header (Udp1553Header) + concatenazione dei messaggi (header1553 + payload + inverted_cw + ctrl_end) + END_1553 (marker finale) IMPORTANTE: Il frame counter deve incrementare ad ogni invio come nel vecchio codice! """ # Crea header con contatore incrementale header = Udp1553Header() header.msg_counter = self.frame_counter self.frame_counter += 1 packed = bytes(header) for msg in messages: packed += msg.pack() # Append global END marker (little-endian uint16) packed += struct.pack("