SXXXXXXX_PyBusMonitor1553/doc/Technical-Architecture.md

24 KiB

PyBusMonitor1553 - Architettura Tecnica e Scelte Implementative

Documento di riferimento per sviluppatori
Versione: 1.0
Data: Dicembre 2025


Indice

  1. Panoramica Architetturale
  2. Sistema di Messaggi e Field Descriptors
  3. Protocollo UDP1553
  4. Scheduler Multi-Rate
  5. Disaccoppiamento GUI/Motore
  6. Threading e Sicurezza
  7. 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:

  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):

// File: th_b1553_icd.h
typedef idd_bitfield_u16_t<IDD_REVBIT16(6), 3, des_control_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

// 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):

  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:

# 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 Configurazione Iniziale Radar

CRITICO: Il radar GRIFO-F/TH richiede configurazione iniziale specifica per uscire da standby:

# 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:
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)
  1. Registrare in lib1553/messages/__init__.py:
from .msg_xx import MsgXX
  1. Aggiungere al dispatcher in core/dispatcher.py:
msgs.MsgXX,  # nella lista message_classes
  1. Aggiungere allo scheduler se BC→RT:
(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
  • Tkinter Threading: Tkinter Thread Safety

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:

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:

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:

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