# PyBusMonitor1553 - Architettura Tecnica e Scelte Implementative **Documento di riferimento per sviluppatori** *Versione: 1.0* *Data: Dicembre 2025* --- ## Indice 1. [Panoramica Architetturale](#panoramica-architetturale) 2. [Sistema di Messaggi e Field Descriptors](#sistema-di-messaggi-e-field-descriptors) - 2.4 [⚠️ Discrepanze ICD vs Implementazione C++](#24--discrepanze-icd-vs-implementazione-c-grifoscope) 3. [Protocollo UDP1553](#protocollo-udp1553) 4. [Scheduler Multi-Rate](#scheduler-multi-rate) 5. [Disaccoppiamento GUI/Motore](#disaccoppiamento-gui-motore) 6. [Threading e Sicurezza](#threading-e-sicurezza) 7. [Testing e Debug](#testing-e-debug) --- ## 1. Panoramica Architetturale ### 1.1 Struttura a Livelli ``` ┌─────────────────────────────────────────────────┐ │ GUI Layer (Tkinter) │ │ main_window.py + gui.py │ └────────────────┬────────────────────────────────┘ │ Queue-based Updates ┌────────────────▼────────────────────────────────┐ │ Core Logic Layer │ │ RadarController + TrafficScheduler │ └────────┬──────────────────┬─────────────────────┘ │ │ │ │ UDP Network ▼ ▼ ┌─────────────────┐ ┌──────────────────┐ │ MessageDispatcher│ │ UdpHandler │ │ (RX Parser) │ │ (Network I/O) │ └────────┬────────┘ └──────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ Protocol Layer (lib1553) │ │ MessageBase + Field Descriptors + Constants │ └─────────────────────────────────────────────────┘ ``` ### 1.2 Flusso Dati **Ricezione (RT→BC):** ``` UDP Socket → UdpHandler.receive() → MessageDispatcher.parse_packet() → Message Objects → GUI Update Queue → Periodic GUI Refresh ``` **Trasmissione (BC→RT):** ``` RadarController.msg_aX → TrafficScheduler (multi-rate) → PacketBuilder → UdpHandler.send() → UDP Socket ``` --- ## 2. Sistema di Messaggi e Field Descriptors ### 2.1 Perché i Descriptors? **Problema**: I messaggi MIL-STD-1553 sono strutture a livello bit complesse. Gestire manualmente bit shifting, maschere e scaling è soggetto a errori. **Soluzione**: Uso di Python Descriptors per: - **Dichiarativo**: Definire campi in modo leggibile - **Type-safe**: Validazione automatica tramite Enum - **Scalabile**: Facile aggiungere nuovi messaggi - **Manutenibile**: ICD e codice allineati ### 2.2 Implementazione Field Descriptors **File**: `lib1553/fields.py` ```python class BitField: # Gestisce conversione MSB-0 (1553) → LSB-0 (Python) # Esempio: Bit 0 (MSB) con width=1 → shift=15 class EnumField: # Mapping automatico int ↔ IntEnum # Gestisce valori non definiti (reserved) class ScaledField: # Conversione raw → physical con LSB # Supporta signed (2's complement) # Esempio: raw=100, lsb=0.5 → value=50.0 ``` **Convenzione Bit Numbering**: - **ICD MIL-STD-1553**: Bit 0 = MSB (Most Significant Bit) - **Python**: Bit 0 = LSB (Least Significant Bit) - **Field base class**: Gestisce automaticamente la conversione con `_shift = 16 - (start_bit + width)` ### 2.3 Esempio Pratico ```python class MsgA2(MessageBase): # ICD: Word 01, Bits 0-3 = Master Mode master_mode = EnumField( word_index=0, # Primo word (0-based) start_bit=0, # MSB del word (convenzione 1553) width=4, # 4 bit = 16 valori possibili enum_cls=RadarMode ) ``` Internamente: 1. `_shift = 16 - (0 + 4) = 12` 2. `_mask = (1 << 4) - 1 = 0x0F` 3. `get: (word >> 12) & 0x0F` 4. `set: word = (word & ~(0x0F << 12)) | ((value & 0x0F) << 12)` ### 2.4 ⚠️ Discrepanze ICD vs Implementazione C++ (GrifoScope) **IMPORTANTE**: Durante lo sviluppo sono emerse differenze tra la specifica ICD documentale e l'implementazione C++ di riferimento (GrifoScope). **La scelta progettuale è stata di seguire l'implementazione C++** in quanto: 1. GrifoScope è il software di riferimento funzionante e testato 2. Il simulatore radar risponde ai comandi dell'implementazione C++ 3. La compatibilità binaria con il sistema esistente è critica #### Discrepanze Documentate | Campo | ICD Documento | Implementazione C++ | Scelta Adottata | File Modificati | |-------|---------------|---------------------|-----------------|-----------------| | **A2/01 Designation Control** | 4 bits (04-07)
NOT_VALID = 15 | 3 bits (04-06)
NOT_VALID = 7 | **C++: 3 bits** | `constants.py`
`msg_a2.py` | | **B7/01 Designation Status** | 4 bits (04-07) | 3 bits (04-06) | **C++: 3 bits** | `msg_b7.py` | **Analisi Codice C++ (GrifoScope)**: ```cpp // File: th_b1553_icd.h typedef idd_bitfield_u16_t des_control_field_t; // ^^^ Width = 3 bits! enum des_control_t { LOCK_ON, // 0 LOCK_ON_DTT, // 1 TRANS_HPT_BUT, // 2 REJECT, // 3 LOCK_ON_STT, // 4 DES_TRACK_LABEL, // 5 EXEC_SAR, // 6 NOT_VALID // 7 (massimo con 3 bits) }; ``` **Impatto sui Bit Successivi**: Con la riduzione da 4 a 3 bits, tutti i campi successivi in Word A2/01 e B7/01 sono stati riallineati: ``` Campo ICD (errato) C++ (corretto) Implementazione Python ───────────────────────────────────────────────────────────────────────── Master Mode 00-03 00-03 start_bit=0, width=4 Designation 04-07 04-06 start_bit=4, width=3 ✓ INT-BIT 08 07 start_bit=7, width=1 ✓ STBY 09 08 start_bit=8, width=1 ✓ FREEZE 10 09 start_bit=9, width=1 ✓ Power-Up Stop 11 10 start_bit=10, width=1 ✓ Reserved 12 11 start_bit=11, width=1 ✓ Silence 13 12 start_bit=12, width=1 ✓ ``` **Verifica Binaria**: È stato creato il test `tools/compare_a2_messages.py` che verifica bit-per-bit la corrispondenza: ``` Phase 1 (STBY=ON, SILENCE=1): Expected (C++): 0x0E88 Actual (Python): 0x0E88 ✓ MATCH Phase 2 (STBY=OFF, SILENCE=0): Expected (C++): 0x0E00 Actual (Python): 0x0E00 ✓ MATCH ``` **Raccomandazioni Future**: - ⚠️ NON modificare questi campi basandosi solo sull'ICD documento - ✅ Verificare SEMPRE con l'implementazione C++ (`th_b1553_icd.h`) - ✅ Eseguire test binari di compatibilità prima di modificare strutture messaggi - 📝 Segnalare eventuali nuove discrepanze in questa sezione **Data Scoperta**: Dicembre 2025 **Riferimenti**: - `___OLD/_old/cpp/GrifoScope/GrifoSdkEif/pub/TH/th_b1553_icd.h` (linee 434-444, 2918-2930) - `doc/ICD_DECD_FTH - GRIFO-F_TH, Data Exchange Control Document for, rev -A, Draft 2.md` (sezione 7.1.2.1) --- ## 3. Protocollo UDP1553 ### 3.1 Struttura Frame ``` ┌──────────────────────────────────────────┐ │ UDP Header (64 bytes, Little Endian) │ │ - marker1553 = 0x1553 │ │ - fcounter, mcounter, etc. │ ├──────────────────────────────────────────┤ │ Message Block 1: │ │ - Begin Marker (0x3C3C) │ │ - Command Word │ │ - Status Word │ │ - Error Code │ │ - Data (0-64 bytes, Big Endian!) │ │ - ~CW (inverted command word) │ │ - End Marker (0x3E3E) │ ├──────────────────────────────────────────┤ │ Message Block 2... │ ├──────────────────────────────────────────┤ │ End Marker (0x5315) │ └──────────────────────────────────────────┘ ``` ### 3.2 Endianness Critico! **Perché due endianness diverse?** 1. **UDP Header (Little Endian)**: - Usa `ctypes.Structure` che segue host endianness (x86 = Little) - Contatori e metadati gestiti dal BC/RT locale 2. **Message Payload (Big Endian - Network Byte Order)**: - Standard MIL-STD-1553 usa Big Endian - Compatibilità con hardware avionico - `MessageBase.pack()`: `struct.pack(">32H", ...)` (nota il `>`) - `MessageBase.unpack()`: `struct.unpack(">32H", ...)` **Errore comune**: Usare Little Endian per il payload → dati scrambled! ### 3.3 Command Word Bitfield Layout ```c // Layout C/C++ (Little Endian bitfield packing) struct CommandWord { uint16_t word_count : 5; // Bits 0-4 uint16_t subaddress : 5; // Bits 5-9 uint16_t tr_bit : 1; // Bit 10 uint16_t remote_terminal : 5; // Bits 11-15 }; ``` **Nota**: In Python usiamo `ctypes.c_uint16` con bitfield per compatibilità esatta. --- ## 4. Scheduler Multi-Rate ### 4.1 Problema Originale Versione iniziale: tutti i messaggi a 1 Hz (debug). **Requisito ICD**: - A1, A3, A7, A8, B6: **6.25 Hz** (160ms) - A2, B7: **25 Hz** (40ms) - A4, A5: **50 Hz** (20ms) ### 4.2 Soluzione: Tick-Based Scheduling **Base Tick Rate**: 200 Hz (5ms) - divisore comune di tutti i rate. ``` Rate Period Ticks/Message Divisor 50 Hz 20ms 4 ticks 4 25 Hz 40ms 8 ticks 8 6.25Hz 160ms 32 ticks 32 ``` **Algoritmo**: ```python for tick in infinite_loop: for (msg, divisor) in schedule_table: if tick % divisor == 0: send(msg) tick += 1 sleep_until_next_tick() # Timing preciso con perf_counter ``` ### 4.3 Raggruppamento Frame I messaggi che cadono nello stesso tick vengono **raggruppati in un unico frame UDP**: - Riduce overhead di rete - Mantiene atomicità temporale - Esempio: Tick 8 → [A2, A4, A5, B7] in un frame ### 4.4 Timing Preciso Usa `time.perf_counter()` invece di `time.sleep()` naïve: ```python next_tick_time = time.perf_counter() while running: # ... send messages ... next_tick_time += BASE_TICK_PERIOD # 5ms sleep_time = next_tick_time - time.perf_counter() if sleep_time > 0: time.sleep(sleep_time) else: # In ritardo, resync next_tick_time = time.perf_counter() ``` **Vantaggi**: - Drift temporale minimizzato - Jitter < 1ms su sistema non real-time - Auto-recupero se il sistema rallenta --- ## 5. Disaccoppiamento GUI/Motore ### 5.1 Problema Originale **Sintomo**: GUI progressivamente meno reattiva fino al blocco. **Causa**: - Ogni messaggio generava `root.after(0, callback)` - A 50Hz → 50+ eventi/secondo nella coda Tkinter - Accumulo di migliaia di eventi pending → freeze ### 5.2 Soluzione: Queue + Rate Limiting **Pattern Producer-Consumer**: ``` Network Thread (Producer) GUI Thread (Consumer) │ │ ├─ on_packet() │ │ └─ queue.put_nowait() │ │ │ │ ┌────▼─────┐ │ │ Timer │ │ │ 100ms │ │ └────┬─────┘ │ │ │ ┌─────────▼────────────┐ │ │ _process_queue() │ │ │ - Batch max 50 msgs │ │ │ - Update TreeView │ │ │ - Update Details │ │ └──────────────────────┘ ``` **Implementazione** (`gui/main_window.py`): ```python class BusMonitorApp: def __init__(self): self._update_queue = queue.Queue(maxsize=1000) self._status_queue = queue.Queue(maxsize=100) self._gui_update_interval_ms = 100 # Rate limiting self._start_gui_update_timer() def queue_message_update(self, msg_name, msg_obj, raw_words): """Thread-safe: chiamato da network thread""" try: self._update_queue.put_nowait((msg_name, msg_obj, raw_words)) except queue.Full: pass # Scarta se pieno, non bloccare! def _process_update_queue(self): """Chiamato SOLO dal thread GUI ogni 100ms""" # Batch processing: max 50 aggiornamenti/ciclo for _ in range(50): try: msg_name, msg_obj, raw_words = self._update_queue.get_nowait() self._update_message_stats_internal(msg_name, msg_obj, raw_words) except queue.Empty: break # Riprogramma prossimo ciclo self._update_id = self.root.after(100, self._process_update_queue) ``` ### 5.3 Ottimizzazione Detail View **Problema**: Ricostruire tutta la vista dettagli ogni aggiornamento è costoso. **Soluzione**: Cached Widget Pattern ```python # Prima selezione di un messaggio if self._current_detail_message != msg_name: self._build_detail_table(msg_name, msg_obj) # Costruisce layout completo self._current_detail_message = msg_name # Aggiornamenti successivi (VELOCE) self._update_detail_values(msg_name, msg_obj) # Solo label.config() ``` **Struttura Dati**: ```python _detail_widgets_cache = { 'A1': { 'master_mode': (raw_label, decoded_label, field_desc), 'symbol_intensity': (raw_label, decoded_label, field_desc), # ... } } ``` **Vantaggi**: - ✅ **10-100x più veloce**: Solo update testo invece di create/pack widgets - ✅ **Meno flicker**: Layout stabile - ✅ **Memoria minima**: Cache solo riferimenti ai widget ### 5.4 Tabella Dettagli Messaggi Layout fisso con 3 colonne: | Field Name | Raw Value | Decoded Value | |-----------------|-----------|------------------------| | master_mode | 0x03 | TWS (3) | | symbol_intensity| 0x7F | 127 | - **Colonna 1**: Nome campo (fisso, creato una volta) - **Colonna 2**: Valore raw in hex (aggiornato ogni messaggio) - **Colonna 3**: Valore decodificato (Enum name, float, etc.) ### 5.5 Vantaggi Architetturali ✅ **Performance costante**: GUI aggiorna a 10Hz fisso, indipendente dal traffico ✅ **No overflow**: Queue limitata, scarta invece di bloccare ✅ **Batch efficiency**: Riduce chiamate a Tkinter (costose) ✅ **Thread-safe**: Separazione completa network/GUI ✅ **Widget caching**: Detail view ottimizzato con layout fisso ### 5.6 Overflow Handling Se i messaggi arrivano a >100Hz (10 msg/100ms): 1. Queue si riempie (1000 elementi) 2. `put_nowait()` lancia `queue.Full` 3. Exception catturata → messaggio scartato 4. **Nessun blocco**, GUI continua a funzionare Accettabile perché: - Statistiche (count, period) rimangono accurate - Dettagli mostrano sempre ultimo valore valido - Alternative peggiori: freeze o crash --- ## 6. Threading e Sicurezza ### 6.1 Thread Attivi 1. **Main Thread**: GUI Tkinter (MUST be main thread) 2. **Network RX Thread**: `UdpHandler._receive_loop()` (daemon) 3. **Scheduler TX Thread**: `TrafficScheduler._loop()` (daemon) ### 6.2 Regole di Sicurezza **CRITICO**: Tkinter NON è thread-safe! ❌ **MAI fare**: ```python # Da network thread def on_packet(data): app.msg_tree.item(...) # CRASH! ``` ✅ **Sempre fare**: ```python # Da network thread def on_packet(data): app.queue_message_update(...) # Thread-safe queue # GUI thread (via timer) def _process_queue(): app.msg_tree.item(...) # OK! ``` ### 6.3 Lock e Sincronizzazione **Non serve Lock esplicito** perché: - `queue.Queue` è già thread-safe - GUI thread è l'unico a modificare widget - Network thread solo accodamento **Eccezione**: Se si aggiunge scrittura condivisa su `RadarController`, serve `threading.Lock`. --- ## 7. Testing e Debug ### 7.1 Debug Flags File `core/network.py`: ```python DEBUG_PACKETS = False # Hex dump di tutti i pacchetti ``` File `core/scheduler.py`: ```python DEBUG_PACKETS = False # Hex dump frame inviati QUIET_MODE = False # Sopprimi log (per GUI) ``` File `core/packet_builder.py`: ```python DEBUG_BUILD = False # Debug costruzione frame ``` ### 7.2 Testing Pattern **Unit Test** (`tests/test_fields.py`): ```python class DummyMsg(MessageBase): field = BitField(word_index=0, start_bit=0, width=8) def test_bitfield(): msg = DummyMsg() msg.field = 255 assert msg.field == 255 assert msg._data[0] == (255 << 8) # MSB position ``` **Integration Test** (`tests/test_dispatcher.py`): ```python def test_parse_a1_packet(): # Build ctypes structures udp_hdr = UDP1553Header() cw = CommandWordUnion(rt_addr=20, sub_addr=1, ...) msg_hdr = UDP1553MessageHeader(cw) # Pack to bytes raw = bytes(udp_hdr) + bytes(msg_hdr) + data_payload # Parse header, msg = dispatcher.parse_packet(raw) assert msg.__class__.__name__ == "MsgA1" ``` ### 7.3 Environment Variables Configurazione network senza modificare codice: ```bash # Windows PowerShell $env:PYBM_RX_IP="192.168.1.100" $env:PYBM_RX_PORT="61553" $env:PYBM_TARGET_IP="192.168.1.200" $env:PYBM_TARGET_PORT="51553" python -m pybusmonitor1553 ``` ### 7.4 Strumenti Diagnostici **UDP Diagnostic Tool** (`tools/udp_diagnostic.py`): - Invia pacchetti di test - Verifica connettività - Dump binario dei frame **Coverage Report**: ```bash pytest --cov=pybusmonitor1553 --cov-report=html # Apri htmlcov/index.html ``` --- ## 8. Best Practices per Nuovi Sviluppatori ### 8.1 Configurazione Iniziale Radar **CRITICO**: Il radar GRIFO-F/TH richiede configurazione iniziale specifica per uscire da standby: ```python # A2: Command - DEVE avere standby = OFF msg_a2.standby_cmd = StandbyStatus.OFF # CRITICAL! msg_a2.master_mode = RadarMode.RWS msg_a2.range_scale = RangeScale.SCALE_20 msg_a2.bar_scan = BarScan.BAR_1 msg_a2.azimuth_scan = AzimuthScan.AZ_60 # A4: Nav Data - Validity flags = 0 significa VALIDO (logica inversa!) msg_a4.nav_data_invalid = 0 # 0 = DATA VALID msg_a4.baro_inertial_alt = 10000.0 # Fornire dati plausibili msg_a4.tas = 300.0 ``` **Nota**: I bit di validità hanno logica inversa: `0 = VALID`, `1 = INVALID`. ### 8.2 Aggiungere un Nuovo Messaggio 1. **Creare** `lib1553/messages/msg_xx.py`: ```python class MsgXX(MessageBase): SUBADDRESS = Subaddress.XX IS_TRANSMIT = True/False RATE_HZ = 25.0 field_name = EnumField(word_index=0, start_bit=0, width=4, enum_cls=MyEnum) ``` 2. **Registrare** in `lib1553/messages/__init__.py`: ```python from .msg_xx import MsgXX ``` 3. **Aggiungere al dispatcher** in `core/dispatcher.py`: ```python msgs.MsgXX, # nella lista message_classes ``` 4. **Aggiungere allo scheduler** se BC→RT: ```python (self.controller.msg_xx, 25.0), # in _build_schedule_table ``` ### 8.2 Debugging Checklist - [ ] `DEBUG_PACKETS = True` per vedere hex dump - [ ] Verificare endianness: payload deve essere Big Endian - [ ] Testare field con valori min/max/invalid - [ ] Controllare bit numbering (MSB=0 in ICD) - [ ] Verificare RATE_HZ e scheduling ### 8.3 Convenzioni Codice - **Naming**: `snake_case` per tutto (Python PEP8) - **Enums**: Sempre `IntEnum` in `constants.py` - **Comments**: Referenziare ICD (es. "Ref. Tab. 7.1.3.1") - **Docstrings**: Google style, specificare Direction e Rate --- ## 9. Limitazioni Note e Future Improvements ### 9.1 Limitazioni 1. **Non Real-Time**: Python + Tkinter non garantiscono latenza <10ms - OK per monitoring, NON per control loop critico 2. **Single-Threaded GUI**: Tkinter bound a main thread - Limite architetturale, non risolvibile 3. **Queue Overflow**: Messaggi possono essere scartati se >100Hz sostenuto - Trade-off accettabile per evitare freeze ### 9.2 Possibili Miglioramenti - **Database Logging**: Salvare messaggi su SQLite per analisi offline - **Chart Plotting**: Grafici tempo reale con matplotlib - **Config File**: YAML/JSON invece di environment variables - **Multi-RT Support**: Gestire più RT simultanei - **Playback Mode**: Replay da file logged --- ## 10. Riferimenti - **MIL-STD-1553**: Interface Standard, Digital Time Division Command/Response Multiplex Data Bus - **ICD GRIFO-F/TH**: Data Exchange Control Document (vedi `doc/ICD_DECD_FTH...pdf`) - **Python Descriptors**: [PEP 252](https://peps.python.org/pep-0252/) - **Tkinter Threading**: [Tkinter Thread Safety](https://tkdocs.com/tutorial/concepts.html#thread) --- ## 11. Troubleshooting - Problemi Risolti ### 11.1 Radar rimane in STANDBY **Sintomo**: Il radar riceve messaggi ma rimane in modalità standby, non risponde ai comandi. **Causa**: `standby_cmd` in A2 non impostato esplicitamente su `OFF`. **Soluzione**: ```python msg_a2.standby_cmd = StandbyStatus.OFF # CRITICAL! ``` ### 11.2 Range Scale / Bar Scan non visualizzati **Sintomo**: MFD del radar mostra "---" per Range Scale, Bars, Azimuth Scan. **Causa**: Campi non inizializzati (valore 0 potrebbe non essere valido per alcuni enum). **Soluzione**: ```python msg_a2.range_scale = RangeScale.SCALE_20 msg_a2.bar_scan = BarScan.BAR_1 msg_a2.azimuth_scan = AzimuthScan.AZ_60 ``` ### 11.3 Nav Data non accettati dal radar **Sintomo**: Radar ignora dati di navigazione, non visualizza posizione/altitudine. **Causa**: Bit di validità non impostati correttamente (logica inversa: 0=VALID, 1=INVALID). **Soluzione**: ```python # Tutti i validity bits devono essere 0 per dati validi msg_a4.nav_data_invalid = 0 msg_a4.attitude_data_invalid = 0 msg_a4.baro_inertial_alt_invalid = 0 # ... e fornire dati plausibili msg_a4.baro_inertial_alt = 10000.0 msg_a4.tas = 300.0 ``` ### 11.4 GUI progressivamente lenta / freeze **Sintomo**: Interfaccia diventa sempre meno reattiva fino a bloccarsi. **Causa**: `root.after()` chiamato da thread esterni accumula eventi nella coda Tkinter. **Soluzione**: Implementato pattern Queue + Rate Limiting (vedi sezione 5). ### 11.5 Messaggi inviati a rate sbagliato **Sintomo**: Tutti i messaggi inviati a 1Hz invece dei rate ICD. **Causa**: Scheduler originale con sleep fisso a 1 secondo. **Soluzione**: Implementato tick-based scheduler a 200Hz (vedi sezione 4). --- ## Conclusione Questo documento descrive le scelte architetturali e implementative di PyBusMonitor1553. Le decisioni principali (field descriptors, multi-rate scheduler, GUI disaccoppiamento) sono motivate da requisiti specifici: - **Correttezza**: Aderenza al protocollo MIL-STD-1553 e ICD - **Performance**: GUI reattiva anche sotto carico - **Manutenibilità**: Codice dichiarativo e testabile - **Estensibilità**: Facile aggiungere nuovi messaggi Per domande o chiarimenti, consultare il codice commentato o contattare i maintainer del progetto.