# 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.