Construyendo un Simulador de Computación Espacial con Claude Code
Quería entender cómo funcionan las computadoras de las naves espaciales. La planificación en tiempo real, el endurecimiento contra radiación, las redes tolerantes a demoras que mantienen a los rovers de Marte en comunicación con la Tierra. Con Artemis II en el horizonte (la primera misión lunar tripulada en más de 50 años) parecía el momento adecuado para profundizar. Así que construí un simulador. Desde cero. Con Claude Code como mi programador par.
Sin hardware físico. Todo corre en QEMU y Docker en una laptop. Código en 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)"]
Por Qué Este Proyecto
Quería aprender algo genuinamente difícil. Algo donde los conceptos son desconocidos y las herramientas son implacables. Compiladores de C que apuntan a ARM. Scripts de enlazador. E/S mapeada en memoria. Tablas de vectores de interrupción.
Claude Code hizo esto posible. No porque escribiera todo el código, sino porque explicó qué hace un script de enlazador mientras estábamos escribiendo uno, de modo que la explicación estaba anclada en el problema real que estaba resolviendo. Lo mismo para volatile, la apropiación preventiva por prioridades de FreeRTOS y los temporizadores de retransmisión de LTP.
La Hoja de Ruta
Dividí el proyecto en cuatro fases, cada una construyendo sobre la anterior:
| Fase | Qué | Conceptos Clave |
|---|---|---|
| Bare Metal | Compilación cruzada ARM, salida UART, manejadores de interrupción | E/S mapeada en memoria, temporizador SysTick, ensamblador de inicio |
| FreeRTOS | Tareas, colas, watchdogs, inversión de prioridades | Planificación determinista, protocolos mutex, apropiación preventiva |
| DTN | Red de dos nodos, enlaces degradados, CFDP, enrutamiento por grafo de contacto | Bundle Protocol, almacenar y reenviar, confiabilidad LTP |
| Integración | Pipeline de telemetría completo con retrasos a distancia de Marte | Puente UART, tc netem, ION OWLT sincronizado |
Cada fase tiene pruebas automatizadas. Cada hito es un PR con CI aprobado.
Fase 1: Bare Metal
El primer desafío fue conseguir que algo funcionara. Compilación cruzada ARM apuntando a un Cortex-M3 (MPS2-AN385) en QEMU. Sin SO, sin biblioteca estándar, sin printf.
Claude me ayudó a entender la secuencia de inicio: la tabla de vectores, el manejador de reset, copiar .data de flash a RAM, poner a cero .bss. Cosas que suceden antes de que main() siquiera se ejecute.
La primera victoria fue un solo carácter apareciendo en una consola UART:
#define UART0_DR (*(volatile uint32_t *)0x40004000)
void uart_putc(char c) {
UART0_DR = c;
}
Dos líneas de C. Sin bibliotecas. Solo escribir un byte en una dirección de memoria que resulta estar conectada a un puerto serie. Se sentía como hablar directamente con la máquina.
Desde allí: interrupciones del temporizador SysTick, manejadores de interrupción, salida estructurada. La demo de SysTick configura un temporizador de hardware para disparar a 2 Hz y cuenta 10 tics:
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.
Sin tics perdidos. Sin tics extra. La CPU duerme entre interrupciones y el hardware la despierta en exactamente el momento correcto. Ejecución determinista desde bare metal.
Fase 2: FreeRTOS
Con bare metal funcionando, agregué FreeRTOS, un sistema operativo en tiempo real que corre en todo, desde dispositivos médicos hasta satélites.
Los ejercicios se construyeron progresivamente:
- Dos tareas a diferentes ritmos. Multitarea básica.
- Comunicación basada en colas. Compartir datos entre tareas de forma segura.
- Temporizador watchdog. Detectar y recuperarse de tareas bloqueadas.
- Pipeline de sensores. Cuatro sensores a diferentes ritmos alimentando una cadena de procesamiento.
- Inversión de prioridades. Desencadenar y resolver el bug clásico de RTOS.
Aquí está el pipeline de sensores arrancando. Nótese el giroscopio a 10 Hz dominando el flujo, con la lectura de temperatura a 1 Hz apareciendo en el tic 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
Eso es la apropiación preventiva basada en prioridades en acción. El giroscopio corre a prioridad 4, el sensor de temperatura a prioridad 1. La lectura de temperatura solo pasa cuando el giroscopio no está ocupando la CPU.
El ejercicio de inversión de prioridades fue el más instructivo. Creé tres tareas donde una tarea de baja prioridad sostiene un mutex que una tarea de alta prioridad necesita, y una tarea de prioridad media muere de inanición junto a ambas. La solución: herencia de prioridades, donde FreeRTOS temporalmente eleva la tarea de baja prioridad para que pueda liberar el mutex más rápido.
Este es el bug que casi mató la misión Mars Pathfinder en 1997. Construirlo yo mismo hizo que la explicación del libro de texto encajara.
Fase 3: Redes Tolerantes a Demoras
TCP/IP no funciona en el espacio. Los tiempos de ida y vuelta a Marte van de 6 a 44 minutos. Los enlaces caen durante horas cuando los planetas ocluyen la señal. La suposición de TCP de una conexión continua y de baja latencia se rompe completamente.
La respuesta de NASA JPL es DTN, o Redes Tolerantes a Demoras. El Bundle Protocol almacena datos localmente y los reenvía salto a salto cuando los enlaces están disponibles. Está diseñado exactamente para las condiciones que rompen internet.
Construí la implementación ION de NASA en Docker y ejecuté pruebas progresivamente más difíciles:
- Conectividad básica. Dos nodos intercambiando bundles.
- Enlaces degradados.
tc netemagregando 500ms de latencia, 25% de pérdida de paquetes, interrupciones completas. - Transferencia de archivos CFDP. Entrega confiable de archivos con verificaciones de integridad.
- Enrutamiento por grafo de contacto. Bundles en cola durante brechas de enlace, entregados cuando se abren las ventanas.
La prueba de enlace intermitente cuenta toda la historia de DTN en pocas líneas de salida:
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
Eso es almacenar y reenviar en acción. El bundle se envía mientras el enlace está completamente muerto, 100% de pérdida de paquetes. Se queda en la cola del nodo local. En el momento en que limpiamos la regla netem y restauramos la conectividad, LTP retransmite y el bundle llega al otro extremo. TCP se habría rendido hace mucho.
Fase 4: Integración
La fase final conecta todo. El firmware FreeRTOS genera telemetría en QEMU. Un script puente en Python lee la salida UART e inyecta como bundles DTN. Los bundles atraviesan una red con demoras para llegar a una estación terrestre.
El firmware arranca e inmediatamente comienza a transmitir:
# 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
Cuatro sensores a cuatro ritmos diferentes. Se puede ver el giroscopio a 10 Hz produciendo la mayoría de las lecturas, con el sensor solar a 2 Hz, temperatura a 1 Hz y batería a 0.5 Hz intercalados. El puente agrupa estos en bundles DTN cada 2 segundos.
La prueba de retraso de Marte prueba que todo el pipeline funciona bajo condiciones realistas:
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 bundles enviados, pero solo 8 entregados para cuando el puente salió. El resto todavía estaba en tránsito a través del retraso de 5 segundos. Ese es el retraso siendo real, no simulado en software.
La simulación de retraso de Marte usa dos mecanismos sincronizados:
- tc netem agrega 5 segundos de latencia de red real a ambos contenedores
- Tablas de rango ION establecen el mismo tiempo de luz unidireccional de 5 segundos para que los temporizadores de retransmisión LTP sean correctos
Ambos deben coincidir. Si el retraso real es de 5 segundos pero ION cree que es de 1 segundo, LTP retransmite agresivamente e inunda el enlace. Conseguir esto correcto me enseñó más sobre diseño de protocolos que cualquier capítulo de libro de texto sobre confiabilidad.
Trabajando con Claude Code
Este proyecto me habría tomado meses sin Claude. No porque el código sea complejo (la mayoría de los archivos tienen menos de 300 líneas) sino porque la curva de aprendizaje para cada dominio es empinada.
Lo que funcionó bien:
- Conocimiento entre dominios. El proyecto abarca C, Python, ensamblador ARM, Docker, ION DTN, FreeRTOS, QEMU, tc netem y LTP. Ninguna persona conoce todos estos bien. Claude podía cambiar de contexto entre ellos.
- Hitos guiados por pruebas. Cada fase tiene pruebas automatizadas. Claude ayudó a escribir aserciones que verifican el comportamiento real, no solo “compila”.
- Flujo de trabajo basado en PR. Cada hito es una rama y PR separados con CI. El soporte de worktree de Claude Code mantuvo esto limpio. Ramas aisladas, sin contaminación cruzada accidental.
Lo que requirió cuidado:
- El C embebido es implacable. Los errores de off-by-one en scripts de enlazador no te dan un stack trace. Te dan un hard fault o corrupción silenciosa. Las sugerencias de Claude necesitaban verificación cuidadosa contra la documentación de hardware.
- La documentación de ION DTN es escasa. Los datos de entrenamiento de Claude no incluyen los internos profundos de ION. Me apoyé más en el código fuente de ION y ejemplos de configuración que en las explicaciones de Claude sobre el comportamiento específico de ION.
Los Números
| Métrica | Valor |
|---|---|
| Total de pruebas | 50+ aserciones en 7 suites de prueba |
| Lenguajes | C, Python, Bash |
| Hardware | Ninguno (QEMU + Docker) |
| Líneas de C | ~1,200 (firmware + bare metal) |
| Líneas de Python | ~1,500 (pruebas + puente + scripts DTN) |
| Config ION DTN | ~350 líneas en 6 archivos .rc |
Pruébalo
Todo es código abierto y corre en cualquier máquina Linux con QEMU y 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
Sin hardware físico. Sin servicios en la nube. Solo una laptop y curiosidad sobre cómo funcionan las computadoras de las naves espaciales.