23 KiB
Guida all'Architettura e al Codice Sorgente del Radar Target Simulator
Questo documento descrive l'architettura interna, le decisioni di progettazione e la struttura del codice sorgente dell'applicazione Radar Target Simulator. È destinato agli sviluppatori e agli ingegneri che necessitano di comprendere, manutenere o estendere il software.
1. Filosofia del Progetto e Principi Architetturali
L'architettura del software si basa su alcuni principi chiave volti a garantire manutenibilità, estendibilità e robustezza.
1.1. Separation of Concerns (SoC)
Il principio fondamentale è la netta separazione delle responsabilità tra i vari componenti del sistema:
- Interfaccia Utente (GUI): Responsabile solo della presentazione dei dati e della cattura dell'input utente. Non contiene logica di business.
- Logica di Simulazione (Core/Engine): Responsabile del calcolo della cinematica dei target. È completamente agnostica rispetto a come i dati vengono visualizzati o trasmessi.
- Comunicazione (Communicators): Responsabile della traduzione dei comandi di alto livello in messaggi specifici di un protocollo e della gestione della comunicazione a basso livello.
Questa separazione permette, ad esempio, di sostituire l'interfaccia basata su Tkinter con una basata su web senza modificare il motore di simulazione, o di aggiungere un nuovo protocollo di comunicazione senza toccare la GUI.
1.2. Thread Safety e Comunicazione tra Thread
L'applicazione è multi-threaded per garantire che l'interfaccia utente rimanga reattiva durante le operazioni potenzialmente lunghe (come la simulazione o l'attesa di pacchetti di rete).
- GUI Thread (Main Thread): Esegue il loop di Tkinter e gestisce tutti i widget.
- Simulation Thread: Esegue il loop del
SimulationEngineper aggiornare lo stato dei target a intervalli regolari. - Communication Thread(s): Componenti come
SfpTransportavviano i propri thread per l'ascolto non bloccante di pacchetti UDP.
Per garantire una comunicazione sicura tra questi thread, l'architettura si affida a due meccanismi principali:
SimulationStateHub: Un contenitore dati centralizzato e thread-safe (protetto dathreading.Lock). Funge da "single source of truth" per lo stato della simulazione. Tutti i thread scrivono e leggono da questo hub, che agisce come un intermediario sicuro, disaccoppiando i produttori di dati (motore di simulazione, ricevitore di rete) dai consumatori (GUI).queue.Queue: Utilizzata per inviare notifiche o dati che richiedono un'elaborazione immediata dalla GUI (es. aggiornamenti per finestre di debug o notifiche di fine simulazione).
1.3. Modularità e Astrazione
Per facilitare l'estensione, il codice fa uso di astrazioni e interfacce. L'esempio più significativo è l'interfaccia CommunicatorInterface. Qualsiasi protocollo di comunicazione (Seriale, TFTP, SFP, o futuri) deve semplicemente implementare i metodi definiti da questa classe base (connect, disconnect, send_scenario, etc.). Il resto dell'applicazione interagisce solo con l'interfaccia, rendendo l'aggiunta di un nuovo protocollo un'operazione a basso impatto.
1.4. Standard e Convenzioni
Il codice aderisce alle seguenti convenzioni per garantire leggibilità e coerenza:
- Stile del Codice: Segue lo standard PEP8.
- Linguaggio: Nomi di funzioni, variabili, commenti e docstring sono scritti in inglese.
- Tipizzazione: Viene fatto uso di type hints (secondo PEP 484) per migliorare la robustezza e la comprensibilità del codice.
2. Struttura delle Cartelle del Progetto
Il codice sorgente è organizzato in un pacchetto Python (target_simulator) con una struttura modulare che riflette la separazione delle responsabilità.
├── doc/ # Documentazione (manuali, guide come questa)
│ ├── manual/
│ └── user_guide/
├── scenarios/ # File JSON contenenti gli scenari predefiniti
├── target_simulator/ # Codice sorgente principale del pacchetto
│ ├── analysis/ # Moduli per l'analisi dati (es. PerformanceAnalyzer, SimulationStateHub)
│ ├── core/ # Cuore della logica di business e simulazione
│ │ ├── models.py # Definizioni dei dati (Target, Waypoint, Scenario)
│ │ ├── simulation_engine.py # Il motore di calcolo della simulazione
│ │ └── ... (moduli di comunicazione: sfp, serial, tftp)
│ ├── gui/ # Componenti dell'interfaccia utente (widget Tkinter)
│ │ ├── main_view.py # Classe principale della finestra dell'applicazione
│ │ ├── ppi_display.py # Widget per la visualizzazione radar
│ │ └── ... (altre finestre e widget personalizzati)
│ ├── simulation/ # Classi di alto livello per l'orchestrazione (es. SimulationController)
│ ├── utils/ # Funzioni e classi di utilità trasversali
│ │ ├── config_manager.py # Gestione dei file di configurazione
│ │ ├── logger.py # Configurazione del sistema di logging
│ │ └── ... (altre utilità)
│ ├── __init__.py # Entry point del pacchetto
│ ├── __main__.py # Entry point per l'esecuzione dell'applicazione (`python -m target_simulator`)
│ └── config.py # Configurazioni globali e costanti (es. LOGGING_CONFIG)
├── README.md # Readme del progetto
└── requirements.txt # Dipendenze Python
3. Mappa dei Componenti Chiave e Flusso dei Dati
Comprendere come i dati fluiscono attraverso l'applicazione è fondamentale per capire il suo funzionamento. Questa sezione descrive le interazioni tra i componenti principali durante due operazioni chiave: l'esecuzione di una simulazione e la ricezione di dati reali.
3.1. Diagramma Architetturale e Flusso Dati
Il seguente diagramma illustra le relazioni e il flusso di informazioni tra i componenti chiave del sistema.
(Placeholder per l'immagine)
Descrizione dell'Immagine: architettura_dettagliata.png
(Questo è il diagramma descritto nel capitolo precedente del manuale tecnico, che possiamo riutilizzare e dettagliare qui. Mostra i blocchi MainView (GUI), SimulationController, SimulationEngine, SimulationStateHub, CommunicatorManager e le loro interazioni).
3.2. Flusso 1: Esecuzione di una Simulazione (Dati in Uscita)
Questo flusso descrive cosa accade dal momento in cui l'utente preme "Start Live" fino all'invio dei dati dei target.
-
Azione Utente (GUI Thread):
- L'utente clicca il pulsante
Start LivesullaMainView. MainViewdelega la richiesta alSimulationController. Per evitare avvii multipli, imposta un flag (_start_in_progress_main) e aggiorna lo stato dei pulsanti.
- L'utente clicca il pulsante
-
Orchestrazione (SimulationController - Nuovo Thread):
- Il
SimulationControlleravvia un thread in background per non bloccare la GUI. - Reset Radar: Chiama il
CommunicatorManagerper inviare un comando di reset al device under test. - Cattura Origine: Legge lo stato corrente dell'ownship dallo
SimulationStateHube lo salva come "origine della simulazione" fissa chiamandoset_simulation_origin(). - Invio Scenario: Chiama il
CommunicatorManagerper inviare lo stato iniziale di tutti i target. - Avvio Motore: Se i passaggi precedenti hanno successo, crea e avvia un'istanza del
SimulationEnginein un nuovo thread.
- Il
-
Ciclo di Simulazione (SimulationEngine - Thread di Simulazione):
- Il
SimulationEngineentra nel suo loop principale, che si ripete a una frequenza fissa (es. 20 Hz). - Ad ogni "tick":
a. Calcola il
delta_timedall'ultimo tick. b. Chiamascenario.update_state(delta_time)per aggiornare la posizione di ogni target. c. Scrive il nuovo stato(timestamp, x, y, z)di ogni target nelloSimulationStateHubchiamandoadd_simulated_state(). - A intervalli più lenti (es. 1 Hz), definiti dall'utente:
a. Prepara una lista di comandi di aggiornamento (es.
tgtseto payload JSON). b. Invia i comandi tramite l'interfaccia delCommunicatorInterface.
- Il
-
Visualizzazione (GUI Thread):
- Il
_gui_refresh_loopdiMainViewsi esegue periodicamente (es. ogni 40 ms). - Chiama
build_display_data()(inppi_adapter.py). build_display_data()legge dalloSimulationStateHub:- Gli stati più recenti dei target simulati.
- L'origine della simulazione.
- Lo stato corrente dell'ownship.
- Esegue la trasformazione di coordinate (rotazione + traslazione) per calcolare la posizione dei target simulati relativa all'ownship corrente.
MainViewpassa i dati trasformati alPPIDisplay, che aggiorna il canvas.
- Il
3.3. Flusso 2: Ricezione di Dati Reali (Dati in Ingresso)
Questo flusso descrive come i dati inviati dal radar vengono ricevuti, processati e visualizzati.
-
Ricezione di Rete (Thread di Comunicazione):
- Il
SfpTransport(o un altro componente di comunicazione) ha un thread in background in ascolto su una porta UDP. - Quando riceve un pacchetto, lo processa (es. riassembla i frammenti SFP).
- Il
-
Routing del Payload (Thread di Comunicazione):
- Una volta che un payload completo è disponibile, il
SfpTransportlo passa alDebugPayloadRouterin base al suo Flow ID. - Il
DebugPayloadRouterispeziona il payload. Se è un messaggio di stato del radar (es. Flow ID 'R'): a. Decodifica il payload binario in una struttura dati (SfpRisStatusPayload). b. Estrae i dati dell'ownship (posizione, heading, etc.) e dei target reali.
- Una volta che un payload completo è disponibile, il
-
Aggiornamento dello Stato (Thread di Comunicazione):
- Il
DebugPayloadRouteraggiorna loSimulationStateHub(che è thread-safe):- Chiama
set_ownship_state()con i nuovi dati di navigazione. - Per ogni target reale ricevuto, chiama
add_real_state()con la sua posizione. - Chiama
set_antenna_azimuth()per aggiornare la posizione dell'antenna.
- Chiama
- Il
-
Visualizzazione (GUI Thread):
- Il
_gui_refresh_loopdiMainView, al suo ciclo successivo, legge i dati "reali" aggiornati dalloSimulationStateHub. build_display_data()calcola la posizione dei target reali relativa all'ownship.MainViewaggiorna ilPPIDisplay, che disegna i target reali (rossi), e aggiorna la posizione e l'orientamento dell'ownship e dell'antenna.
- Il
Questo doppio flusso di dati, che converge nello SimulationStateHub e viene poi letto dalla GUI, è il cuore del funzionamento in tempo reale dell'applicazione.
4. Analisi Dettagliata dei Moduli Principali
Questa sezione fornisce una scomposizione delle responsabilità e delle interazioni per i moduli e le classi più importanti del progetto.
4.1. Entry Point e Orchestrazione GUI (__main__.py, gui/main_view.py)
-
target_simulator/__main__.py- Responsabilità: È l'entry point principale dell'applicazione. Il suo unico scopo è inizializzare il sistema di logging di base e creare un'istanza della classe
MainView, per poi avviare il loop principale di Tkinter (app.mainloop()).
- Responsabilità: È l'entry point principale dell'applicazione. Il suo unico scopo è inizializzare il sistema di logging di base e creare un'istanza della classe
-
target_simulator/gui/main_view.py->MainView(tk.Tk)- Responsabilità: È la classe "Dio" della GUI, ma con responsabilità ben definite. Orchestra tutti i componenti principali, costruisce la finestra e gestisce il ciclo di aggiornamento dell'interfaccia.
- Input/Stato Interno:
- Possiede le istanze uniche di
ConfigManager,SimulationStateHub,CommunicatorManagereSimulationController. - Mantiene lo
Scenariocorrente in memoria.
- Possiede le istanze uniche di
- Output/Side-effect:
- Disegna e aggiorna tutti i widget della GUI.
- Delega le azioni complesse (start/stop) al
SimulationController. - Salva le configurazioni tramite il
ConfigManager.
- Interazioni Principali:
_gui_refresh_loop: È il cuore pulsante della GUI. A intervalli regolari, legge i dati dalloSimulationStateHubtramite ilppi_adaptere aggiorna ilPPIDisplaye altri widget.- Callback degli Eventi: I metodi
_on_*(es._on_start_simulation) rispondono agli input dell'utente e attivano la logica appropriata, solitamente delegando al controller.
4.2. Logica di Simulazione (simulation/, core/simulation_engine.py)
-
target_simulator/simulation/simulation_controller.py->SimulationController- Responsabilità: Agisce come un "regista" per le operazioni di simulazione. Gestisce la logica di alto livello per l'avvio e l'arresto, che coinvolge più componenti e deve avvenire in una sequenza precisa.
- Input: Riceve richieste da
MainView. - Output/Side-effect: Crea e distrugge istanze del
SimulationEngine. - Interazioni Principali:
- Usa il
CommunicatorManagerper inviare comandi di controllo (reset, invio scenario). - Usa lo
SimulationStateHubper impostare l'origine della simulazione.
- Usa il
-
target_simulator/core/simulation_engine.py->SimulationEngine(threading.Thread)- Responsabilità: Eseguire i calcoli cinematici della simulazione in un thread separato per non bloccare la GUI. È il "motore" che fa avanzare il tempo per i target.
- Input: Un oggetto
Scenarioda simulare e unCommunicatorInterfaceper inviare i dati. - Output/Side-effect: Scrive continuamente gli stati aggiornati dei target nello
SimulationStateHube invia comandi alCommunicatorInterface. - Interazioni Principali:
- Nel suo
run()loop:- Chiama
target.update_state()per ogni target. - Chiama
simulation_hub.add_simulated_state(). - Chiama
communicator.send_commands().
- Chiama
- Nel suo
4.3. Modelli dei Dati (core/models.py)
target_simulator/core/models.py- Responsabilità: Definisce le strutture dati fondamentali dell'applicazione:
Scenario,Target,Waypoint. Queste classi contengono non solo i dati, ma anche la logica per manipolarli. Targetclass: Contiene la logica per calcolare la propria posizione (update_state) e per generare il percorso completo a partire dai waypoint (_generate_path). È il componente che implementa la cinematica.Scenarioclass: È un contenitore diTargetcon metodi helper per gestire l'intero gruppo (es.update_statesu tutti i target).Waypointdataclass: Una semplice struttura dati per descrivere una manovra.
- Responsabilità: Definisce le strutture dati fondamentali dell'applicazione:
4.4. Hub dei Dati (analysis/simulation_state_hub.py)
target_simulator/analysis/simulation_state_hub.py->SimulationStateHub- Responsabilità: Essere il contenitore dati centrale e thread-safe. Disaccoppia i produttori di dati (engine, comunicatori) dai consumatori (GUI). Mantiene una cronologia limitata degli stati per l'analisi e la visualizzazione delle tracce.
- Input: Riceve dati tramite i suoi metodi
add_*_state()eset_*_state(). - Output: Fornisce dati tramite i suoi metodi
get_*(). - Interazioni Principali: È il componente più connesso. Praticamente tutti gli altri moduli principali interagiscono con esso. La sua natura thread-safe (tutti i metodi pubblici usano un
threading.Lock) è cruciale per la stabilità dell'applicazione.
4.5. Comunicazione (core/communicator_manager.py, core/*_communicator.py)
target_simulator/core/communicator_interface.py->CommunicatorInterface- Responsabilità: Definire il contratto (interfaccia astratta) che tutti i moduli di comunicazione devono rispettare.
target_simulator/core/communicator_manager.py->CommunicatorManager- Responsabilità: Funge da "fabbrica" e facciata per i communicator. Crea l'istanza del communicator corretto (SFP, TFTP, etc.) in base alla configurazione e inoltra le chiamate (es.
connect,send_scenario). - Interazioni Principali: Viene usato dal
SimulationControllerper gestire le connessioni e inviare comandi di controllo.
- Responsabilità: Funge da "fabbrica" e facciata per i communicator. Crea l'istanza del communicator corretto (SFP, TFTP, etc.) in base alla configurazione e inoltra le chiamate (es.
SFPCommunicator,TFTPCommunicator,SerialCommunicator- Responsabilità: Ognuna di queste classi implementa la logica specifica per un protocollo.
SFPCommunicator, ad esempio, si affida aSfpTransportper la gestione a basso livello dei pacchetti UDP e dei frammenti.
- Responsabilità: Ognuna di queste classi implementa la logica specifica per un protocollo.
4.6. Visualizzazione (gui/ppi_display.py, gui/ppi_adapter.py)
target_simulator/gui/ppi_display.py->PPIDisplay- Responsabilità: È un widget complesso e auto-contenuto che si occupa esclusivamente di disegnare lo scenario radar su un canvas Matplotlib. Gestisce la grafica di target, tracce, ownship, settore di scansione e antenna.
- Input: Riceve liste di oggetti
Target(già in coordinate relative) dai suoi metodiupdate_simulated_targetseupdate_real_targets. Riceve l'heading dell'ownship per orientare la vista. - Output/Side-effect: Aggiorna il canvas.
target_simulator/gui/ppi_adapter.py->build_display_data- Responsabilità: Funzione helper "pura" che funge da adattatore. Il suo unico scopo è leggere i dati grezzi dallo
SimulationStateHubed eseguire le trasformazioni di coordinate necessarie per preparare i dati nel formato atteso dalPPIDisplay. - Input: L'istanza dello
SimulationStateHub. - Output: Un dizionario contenente le liste di
Targetpronti per essere disegnati.
- Responsabilità: Funzione helper "pura" che funge da adattatore. Il suo unico scopo è leggere i dati grezzi dallo
4.7. Utilità (utils/)
-
target_simulator/utils/config_manager.py->ConfigManager- Responsabilità: Abstrae la lettura e la scrittura dei file di configurazione (
settings.json) e degli scenari (scenarios.json). Gestisce il path dei file e la serializzazione/deserializzazione da/verso JSON.
- Responsabilità: Abstrae la lettura e la scrittura dei file di configurazione (
-
target_simulator/utils/logger.py- Responsabilità: Configura il sistema di logging centralizzato basato su
Queue, permettendo ai thread in background di inviare log in modo sicuro che verranno poi scritti sulla console o sul widget della GUI dal thread principale.
5. Guida all'Estensione del Progetto
- Responsabilità: Configura il sistema di logging centralizzato basato su
Questa sezione fornisce esempi pratici su come estendere l'applicazione con nuove funzionalità, seguendo i principi architetturali stabiliti.
5.1. Caso d'Uso: Aggiungere un Nuovo Tipo di Manovra
Supponiamo di voler aggiungere una nuova manovra, ad esempio "Orbita attorno a un punto".
-
Modificare i Modelli (
core/models.py):- Aggiungere un nuovo valore all'enum
ManeuverType:class ManeuverType(Enum): # ... ORBIT_POINT = "Orbit Point" - Aggiungere i campi necessari alla
dataclass Waypointper supportare la nuova manovra (es.orbit_center_x,orbit_center_y,orbit_radius_nm,orbit_direction).
- Aggiungere un nuovo valore all'enum
-
Aggiornare la Logica di Calcolo (
core/models.py):- Modificare il metodo statico
Target.generate_path_from_waypointsper gestire il nuovoManeuverType.ORBIT_POINT. Qui andrà implementata la logica matematica per generare i punti(t, x, y, z)che descrivono l'orbita.
- Modificare il metodo statico
-
Aggiornare l'Interfaccia Utente (
gui/waypoint_editor_window.py):- Nella classe
WaypointEditorWindow, creare un nuovottk.Framecontenente i widget (Spinbox, Combobox, etc.) per inserire i parametri della nuova manovra (raggio, centro, etc.). - Modificare il metodo
_on_maneuver_type_changeper mostrare questo nuovo frame quando l'utente seleziona "Orbit Point" dal menu a tendina. - Aggiornare il metodo
_on_okper leggere i valori dai nuovi widget e popolare correttamente l'oggettoWaypointquando si salva.
- Nella classe
5.2. Caso d'Uso: Aggiungere un Nuovo Protocollo di Comunicazione
Supponiamo di voler aggiungere il supporto per un nuovo protocollo basato su TCP.
-
Creare la Classe del Communicator (
core/tcp_communicator.py):- Creare un nuovo file
tcp_communicator.pynel modulocore. - Definire una nuova classe
TCPCommunicatorche eredita daCommunicatorInterface(core/communicator_interface.py). - Implementare tutti i metodi astratti richiesti dall'interfaccia:
connect(self, config): Logica per stabilire la connessione TCP.disconnect(self): Logica per chiudere la connessione.is_open(self): Proprietà che restituisce lo stato della connessione.send_scenario(self, scenario): Logica per serializzare e inviare lo stato iniziale dello scenario su TCP.send_commands(self, commands): Logica per inviare gli aggiornamenti in tempo reale.test_connection(config)elist_available_ports(): Metodi statici.
- Creare un nuovo file
-
Aggiornare il Gestore (
core/communicator_manager.py):- Nella classe
CommunicatorManager, modificare il metodo_setup_communicatorper riconoscere il nuovo tipo "tcp".# In _setup_communicator from target_simulator.core.tcp_communicator import TCPCommunicator # ... elif comm_type == "tcp": communicator = TCPCommunicator() config_data = config.get("tcp", {})
- Nella classe
-
Aggiornare l'Interfaccia Utente (
gui/connection_settings_window.py):- Aggiungere "TCP" alla lista dei
valuesnelttk.Comboboxdel tipo di connessione. - Creare un nuovo
ttk.Frame(es.tcp_frame) con i widget per i parametri TCP (IP, porta). - Aggiungere una nuova scheda al
ttk.Notebookper ospitare iltcp_frame. - Aggiornare i metodi
_load_settingse_on_saveper leggere e scrivere la nuova sezione di configurazionetcpnel dizionarioconnection_config. - Aggiungere la logica per
_test_connectionper il tipo "TCP".
- Aggiungere "TCP" alla lista dei
5.3. Flusso di Debug Consigliato
Quando si affronta un bug o si sviluppa una nuova funzionalità, si consiglia il seguente approccio:
- Isolare il Problema: Il bug è nella visualizzazione (GUI), nel calcolo (Engine) o nella comunicazione (Communicator)? La separazione delle responsabilità dovrebbe aiutare a identificare il dominio del problema.
- Aumentare il Livello di Log: Usare la finestra
Debug -> Logger Levels...per impostare aDEBUGil livello di log del modulo sospetto. Ad esempio:- Problemi di visualizzazione? Abilita il debug per
target_simulator.gui.ppi_displayetarget_simulator.gui.ppi_adapter. - Problemi di comunicazione? Abilita il debug per
target_simulator.core.sfp_communicatoretarget_simulator.core.sfp_transport.
- Problemi di visualizzazione? Abilita il debug per
- Usare gli Strumenti di Debug:
- L'
SFP Packet Inspectorè fondamentale per problemi di comunicazione. Controlla la schedaRawper vedere esattamente cosa viene ricevuto e la schedaRISper vedere come viene interpretato. - Usa il
Simple Target Senderper inviare comandi isolati e testare specifiche risposte del sistema radar senza eseguire uno scenario completo.
- L'
- Ispezionare lo
SimulationStateHub: Se c'è una discrepanza tra ciò che il motore dovrebbe calcolare e ciò che la GUI mostra, il problema è probabilmente nelppi_adapter. Se i dati nelloHubstesso sono sbagliati, il problema è a monte (nelSimulationEngineo nelDebugPayloadRouter).
Seguendo questi principi e utilizzando gli strumenti forniti, la manutenzione e l'estensione dell'applicazione dovrebbero risultare più semplici e strutturate.