Costruire un Simulatore di Calcolo Spaziale con Claude Code
Volevo capire come funzionano i computer delle navicelle spaziali. La pianificazione in tempo reale, la protezione dalle radiazioni, le reti tolleranti ai ritardi che mantengono i rover su Marte in comunicazione con la Terra. Con Artemis II all’orizzonte (la prima missione lunare con equipaggio in oltre 50 anni) sembrava il momento giusto per approfondire. Così ho costruito un simulatore. Da zero. Con Claude Code come mio pair programmer.
Nessun hardware fisico. Tutto gira in QEMU e Docker su un laptop. Codice su GitHub.
flowchart LR
QEMU["QEMU (Cortex-M3)"] -->|UART socket| Bridge[uart_bridge.py]
Bridge -->|bpsendfile| SC[Spacecraft Node]
SC -->|LTP + 5s delay| GS[Ground Station]
subgraph Docker
SC
GS
end
flowchart TB
QEMU["QEMU (Cortex-M3)"] -->|UART socket| Bridge[uart_bridge.py]
Bridge -->|bpsendfile| SC["Spacecraft Node (Docker)"]
SC -->|LTP + 5s delay| GS["Ground Station (Docker)"]
Perché Questo Progetto
Volevo imparare qualcosa di genuinamente difficile. Qualcosa in cui i concetti sono sconosciuti e gli strumenti sono implacabili. Compilatori C che puntano ad ARM. Script del linker. I/O mappata in memoria. Tabelle dei vettori di interrupt.
Claude Code ha reso tutto ciò possibile. Non perché scrivesse tutto il codice, ma perché spiegava cosa fa uno script del linker mentre ne stavamo scrivendo uno, così la spiegazione era radicata nel problema reale che stavo risolvendo. Lo stesso per volatile, la preemption per priorità di FreeRTOS e i timer di ritrasmissione LTP.
La Roadmap
Ho suddiviso il progetto in quattro fasi, ognuna costruita sulla precedente:
| Fase | Cosa | Concetti Chiave |
|---|---|---|
| Bare Metal | Cross-compilazione ARM, output UART, gestori di interrupt | I/O mappata in memoria, timer SysTick, assembly di avvio |
| FreeRTOS | Task, code, watchdog, inversione di priorità | Scheduling deterministico, protocolli mutex, preemption |
| DTN | Rete a due nodi, collegamenti degradati, CFDP, routing con grafo dei contatti | Bundle Protocol, store-and-forward, affidabilità LTP |
| Integrazione | Pipeline di telemetria completa con ritardi a distanza di Marte | Bridge UART, tc netem, ION OWLT sincronizzato |
Ogni fase ha test automatizzati. Ogni milestone è una PR con CI superato.
Fase 1: Bare Metal
La prima sfida era far girare qualcosa. Cross-compilazione ARM puntando a un Cortex-M3 (MPS2-AN385) in QEMU. Nessun OS, nessuna libreria standard, nessun printf.
Claude mi ha aiutato a capire la sequenza di avvio: la tabella dei vettori, il gestore di reset, la copia di .data da flash a RAM, l’azzeramento di .bss. Cose che accadono prima che main() venga persino eseguita.
La prima vittoria è stata un singolo carattere che appariva su una console UART:
#define UART0_DR (*(volatile uint32_t *)0x40004000)
void uart_putc(char c) {
UART0_DR = c;
}
Due righe di C. Nessuna libreria. Solo scrivere un byte in un indirizzo di memoria che casualmente è collegato a una porta seriale. Sembrava parlare direttamente con la macchina.
Da lì: interrupt del timer SysTick, gestori di interrupt, output strutturato. La demo SysTick configura un timer hardware per scattare a 2 Hz e conta 10 tick:
SysTick interrupt demo
======================
Ticking at 2 Hz for 5 seconds (10 ticks)...
tick 1
tick 2
tick 3
tick 4
tick 5
tick 6
tick 7
tick 8
tick 9
tick 10
Done — 5 seconds counted by interrupt.
Nessun tick perso. Nessun tick extra. La CPU dorme tra gli interrupt e l’hardware la sveglia esattamente al momento giusto. Esecuzione deterministica dal bare metal.
Fase 2: FreeRTOS
Con il bare metal funzionante, ho aggiunto FreeRTOS, un sistema operativo in tempo reale che gira su tutto, dai dispositivi medici ai satelliti.
Gli esercizi sono stati costruiti progressivamente:
- Due task a ritmi diversi. Multitasking di base.
- Comunicazione basata su code. Condivisione sicura dei dati tra task.
- Timer watchdog. Rilevare e recuperare da task bloccati.
- Pipeline di sensori. Quattro sensori a ritmi diversi che alimentano una catena di elaborazione.
- Inversione di priorità. Innescare e risolvere il bug classico degli RTOS.
Ecco la pipeline di sensori che si avvia. Notate il giroscopio a 10 Hz che domina il flusso, con la lettura della temperatura a 1 Hz inserita al tick 1001:
FreeRTOS Sensor Pipeline Demo
=============================
[GYRO] Sensor online (10 Hz, priority 4)
[PROC] Processor online (priority 3)
[TELEM] Telemetry online (priority 2)
[TEMP] Sensor online (1 Hz, priority 1)
[TELEM] #000 GYRO: 300 at tick 100
[TELEM] #001 GYRO: 300 at tick 200
...
[TELEM] #009 GYRO: 300 at tick 1000
[TELEM] #010 TEMP: 1991 at tick 1001
[TELEM] #011 GYRO: 300 at tick 1100
Questa è la preemption basata sulla priorità in azione. Il giroscopio gira alla priorità 4, il sensore di temperatura alla priorità 1. La lettura della temperatura passa solo quando il giroscopio non occupa la CPU.
L’esercizio sull’inversione di priorità è stato il più istruttivo. Ho creato tre task in cui un task a bassa priorità tiene un mutex di cui ha bisogno un task ad alta priorità, e un task a priorità media li fa morire di fame entrambi. La soluzione: ereditarietà delle priorità, dove FreeRTOS aumenta temporaneamente il task a bassa priorità in modo che possa rilasciare il mutex più velocemente.
Questo è il bug che quasi uccise la missione Mars Pathfinder nel 1997. Costruirlo da solo ha fatto scattare la spiegazione del libro di testo.
Fase 3: Reti Tolleranti ai Ritardi
TCP/IP non funziona nello spazio. I tempi di andata e ritorno verso Marte vanno da 6 a 44 minuti. I collegamenti cadono per ore quando i pianeti occultano il segnale. L’assunzione di TCP di una connessione continua e a bassa latenza si sgretola completamente.
La risposta di NASA JPL è DTN, o Delay-Tolerant Networking. Il Bundle Protocol archivia i dati localmente e li inoltra hop per hop quando i collegamenti diventano disponibili. È progettato esattamente per le condizioni che mandano in tilt internet.
Ho costruito l’implementazione ION della NASA in Docker ed eseguito test progressivamente più difficili:
- Connettività di base. Due nodi che scambiano bundle.
- Collegamenti degradati.
tc netemche aggiunge 500ms di latenza, 25% di perdita di pacchetti, interruzioni complete. - Trasferimento file CFDP. Consegna affidabile dei file con controlli di integrità.
- Routing con grafo dei contatti. Bundle in coda durante le lacune di collegamento, consegnati quando le finestre si aprono.
Il test del collegamento intermittente racconta tutta la storia DTN in poche righe di output:
Test: intermittent link (send during outage, deliver on recovery)...
qdisc: qdisc netem root refcnt 2 limit 1000 loss 100%
confirmed: bundle queued (link is down)
restoring link...
PASS: bundle held during outage
PASS: bundle delivered after link recovery
Questo è lo store-and-forward in azione. Il bundle viene inviato mentre il collegamento è completamente morto, 100% di perdita di pacchetti. Si trova nella coda del nodo locale. Nel momento in cui cancelliamo la regola netem e ripristiniamo la connettività, LTP ritrasmette e il bundle arriva all’altro capo. TCP si sarebbe arreso molto tempo fa.
Fase 4: Integrazione
La fase finale collega tutto. Il firmware FreeRTOS genera telemetria in QEMU. Uno script bridge Python legge l’output UART e lo inietta come bundle DTN. I bundle attraversano una rete ritardata per raggiungere una stazione di terra.
Il firmware si avvia e inizia immediatamente a trasmettere:
# Spacecraft Telemetry Firmware v1.0
# ===================================
# GYRO sensor online (10 Hz, priority 4)
# Processor online (priority 3)
# Telemetry online (priority 2)
# TEMP sensor online (1 Hz, priority 1)
# BATT sensor online (0.5 Hz, priority 1)
# SUN sensor online (2 Hz, priority 1)
$TELEM,0000,GYRO,300,100
$TELEM,0001,GYRO,300,200
...
$TELEM,0005,SUN,801,501
...
$TELEM,0011,TEMP,1991,1001
$TELEM,0012,SUN,801,1001
Quattro sensori a quattro ritmi diversi. Si può vedere il giroscopio a 10 Hz che produce la maggior parte delle letture, con il sensore solare a 2 Hz, la temperatura a 1 Hz e la batteria a 0,5 Hz intercalati. Il bridge raggruppa questi in bundle DTN ogni 2 secondi.
Il test del ritardo di Marte dimostra che l’intera pipeline funziona in condizioni realistiche:
Running Mars-delay end-to-end tests...
PASS: tc netem delay 5s applied on spacecraft
PASS: bridge exited cleanly
PASS: bridge read telemetry lines
PASS: bridge sent >= 1 bundle (got 12)
PASS: ground station received files (got 8)
PASS: received telemetry lines (got 224)
PASS: telemetry CSV format valid (5 fields)
INFO: delay observable (8 delivered at bridge exit < 12 sent)
7 passed, 0 failed
12 bundle inviati, ma solo 8 consegnati quando il bridge è uscito. Il resto era ancora in volo attraverso il ritardo di 5 secondi. Questo è il ritardo che è reale, non simulato nel software.
La simulazione del ritardo di Marte usa due meccanismi sincronizzati:
- tc netem aggiunge 5 secondi di latenza di rete reale a entrambi i container
- Tabelle di portata ION impostano lo stesso tempo di luce unidirezionale di 5 secondi in modo che i timer di ritrasmissione LTP siano corretti
Entrambi devono concordare. Se il ritardo effettivo è di 5 secondi ma ION pensa che sia di 1 secondo, LTP ritrasmette aggressivamente e inonda il collegamento. Farlo bene mi ha insegnato più sul design dei protocolli di qualsiasi capitolo di libro di testo sull’affidabilità.
Lavorare con Claude Code
Questo progetto mi avrebbe richiesto mesi senza Claude. Non perché il codice sia complesso (la maggior parte dei file è sotto le 300 righe) ma perché la curva di apprendimento per ogni dominio è ripida.
Cosa ha funzionato bene:
- Conoscenza cross-dominio. Il progetto abbraccia C, Python, assembly ARM, Docker, ION DTN, FreeRTOS, QEMU, tc netem e LTP. Nessuna singola persona conosce bene tutti questi. Claude poteva passare da uno all’altro.
- Milestone guidati dai test. Ogni fase ha test automatizzati. Claude ha aiutato a scrivere asserzioni che verificano il comportamento reale, non solo “compila”.
- Workflow basato su PR. Ogni milestone è un branch e PR separato con CI. Il supporto worktree di Claude Code ha mantenuto tutto ordinato. Branch isolati, nessuna contaminazione incrociata accidentale.
Cosa ha richiesto attenzione:
- Il C embedded è implacabile. Gli errori off-by-one negli script del linker non ti danno un stack trace. Ti danno un hard fault o una corruzione silenziosa. I suggerimenti di Claude richiedevano una verifica attenta rispetto alla documentazione hardware.
- La documentazione di ION DTN è scarsa. I dati di addestramento di Claude non includono i meccanismi interni profondi di ION. Mi sono affidato più al codice sorgente di ION e agli esempi di configurazione che alle spiegazioni di Claude sul comportamento specifico di ION.
I Numeri
| Metrica | Valore |
|---|---|
| Test totali | 50+ asserzioni in 7 suite di test |
| Linguaggi | C, Python, Bash |
| Hardware | Nessuno (QEMU + Docker) |
| Righe di C | ~1.200 (firmware + bare metal) |
| Righe di Python | ~1.500 (test + bridge + script DTN) |
| Config ION DTN | ~350 righe in 6 file .rc |
Prova
Tutto è open source e gira su qualsiasi macchina Linux con QEMU e Docker:
sudo apt install gcc-arm-none-eabi qemu-system-arm build-essential
git clone https://github.com/granda/spacecraft-computing-sim
cd spacecraft-computing-sim
make -C bare-metal run # bare metal UART output
make -C freertos run # FreeRTOS sensor pipeline
make -C dtn test # two-node DTN network
make -C integration test-bridge # full telemetry pipeline
make -C integration test-mars-delay # Mars-distance delays
Nessun hardware fisico. Nessun servizio cloud. Solo un laptop e curiosità su come funzionano i computer delle navicelle spaziali.