18 KiB
PyBusMonitor1553 - Architettura Tecnica e Scelte Implementative
Documento di riferimento per sviluppatori
Versione: 1.0
Data: Dicembre 2025
Indice
- Panoramica Architetturale
- Sistema di Messaggi e Field Descriptors
- Protocollo UDP1553
- Scheduler Multi-Rate
- Disaccoppiamento GUI/Motore
- Threading e Sicurezza
- 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
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
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:
_shift = 16 - (0 + 4) = 12_mask = (1 << 4) - 1 = 0x0Fget: (word >> 12) & 0x0Fset: word = (word & ~(0x0F << 12)) | ((value & 0x0F) << 12)
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?
-
UDP Header (Little Endian):
- Usa
ctypes.Structureche segue host endianness (x86 = Little) - Contatori e metadati gestiti dal BC/RT locale
- Usa
-
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
// 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:
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:
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):
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
# 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:
_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):
- Queue si riempie (1000 elementi)
put_nowait()lanciaqueue.Full- Exception catturata → messaggio scartato
- 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
- Main Thread: GUI Tkinter (MUST be main thread)
- Network RX Thread:
UdpHandler._receive_loop()(daemon) - Scheduler TX Thread:
TrafficScheduler._loop()(daemon)
6.2 Regole di Sicurezza
CRITICO: Tkinter NON è thread-safe!
❌ MAI fare:
# Da network thread
def on_packet(data):
app.msg_tree.item(...) # CRASH!
✅ Sempre fare:
# 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:
DEBUG_PACKETS = False # Hex dump di tutti i pacchetti
File core/scheduler.py:
DEBUG_PACKETS = False # Hex dump frame inviati
QUIET_MODE = False # Sopprimi log (per GUI)
File core/packet_builder.py:
DEBUG_BUILD = False # Debug costruzione frame
7.2 Testing Pattern
Unit Test (tests/test_fields.py):
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):
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:
# 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:
pytest --cov=pybusmonitor1553 --cov-report=html
# Apri htmlcov/index.html
8. Best Practices per Nuovi Sviluppatori
8.1 Aggiungere un Nuovo Messaggio
- Creare
lib1553/messages/msg_xx.py:
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)
- Registrare in
lib1553/messages/__init__.py:
from .msg_xx import MsgXX
- Aggiungere al dispatcher in
core/dispatcher.py:
msgs.MsgXX, # nella lista message_classes
- Aggiungere allo scheduler se BC→RT:
(self.controller.msg_xx, 25.0), # in _build_schedule_table
8.2 Debugging Checklist
DEBUG_PACKETS = Trueper 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_caseper tutto (Python PEP8) - Enums: Sempre
IntEnuminconstants.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
-
Non Real-Time: Python + Tkinter non garantiscono latenza <10ms
- OK per monitoring, NON per control loop critico
-
Single-Threaded GUI: Tkinter bound a main thread
- Limite architetturale, non risolvibile
-
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
- Tkinter Threading: Tkinter Thread Safety
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.