# 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)
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)`

---

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