# Sprint 1 — Firmware ESP32 base — Plan propuesto > **Status:** propuesto, esperando tu OK. > **Brief reference:** sección 12, Sprint 1. > **Duración estimada brief:** 3-4 semanas. > **Pre-requisito:** Sprint 0 aprobado (tag `sprint-0-approved`). Este documento es **mi propuesta**. Antes de tocar firmware necesito tu OK explícito (regla de oro #1 del brief). Léelo, corrige lo que no te encaje, y luego me das luz verde o me pides cambios. --- ## 1. Objetivo del sprint Levantar la **base** del firmware del controlador AR-Autopilot sobre la tarjeta AR-NMEA-IO v1.0. Al final del sprint la tarjeta debe poder: - Arrancar en modo **STANDBY** (timón manual, sin control del piloto). - Leer los 4 AI + 5 DI + 1 RPM, escribir los 10 DO declarados en [`firmware/ar_autopilot_v1/src/hal/pinout.h`](../firmware/ar_autopilot_v1/src/hal/pinout.h). - Hablar **Modbus RTU como esclavo** con el display dedicado (todavía inexistente en código — usamos un cliente de prueba en PC). - Consumir del bus **NMEA 2000** los PGN mínimos: **127250** (Heading) y **127251** (Rate of Turn). Solo logging, no control. - Reaccionar al pulsador físico de **disengage** (DI1): no hay nada enganchado en Sprint 1, pero el evento se registra y se ven los bits cambiar en Modbus. Lo que **NO** hace este sprint (importante — ver brief sección 12): - No corre PID real. Hay un "stub" del PID que devuelve siempre 0. - No publica al backbone NMEA 2000 (eso es Sprint 6). - No tiene seguridad funcional completa (Sprint 6 también). - No conoce los modos HEADING_HOLD / TRUE_COURSE / TRACK_KEEPING más allá de exponerlos como enum por Modbus. --- ## 2. Decisiones técnicas que pido aprobar Estas son las preguntas que necesito que me respondas **antes** de empezar. ### Decisión 2.1 — Framework: ESP-IDF puro vs Arduino-as-ESP-IDF-component | Opción | Pros | Cons | Veredicto | |---|---|---|---| | **Arduino framework** puro | Más fácil. Las libs NMEA2000_esp32 y eModbus son nativas Arduino. Curva corta | Arduino abstrae FreeRTOS en un modelo single-threaded. Mal para nuestras 4 tareas concurrentes con prioridades distintas. Menos control sobre OTA con rollback, secure boot, deterministic timing | **No** para producto safety-critical | | **ESP-IDF nativo** | Control total de FreeRTOS, deterministic real-time, OTA con rollback automático, secure boot/flash encryption, mejor para producción comercial | Las libs Modbus/NMEA2000 maduras son Arduino-first; hay que wrappear o reescribir partes | Limpio pero costoso | | **Arduino-as-ESP-IDF-component** ⭐ | Combina lo mejor: ESP-IDF maneja el sistema (tareas, prioridades, OTA, security), Arduino se usa solo para las libs maduras (NMEA2000_esp32, eModbus). Patrón oficial Espressif | Configuración inicial un poco más complicada en `platformio.ini` | **Recomendado** | **Mi recomendación:** opción ⭐. El brief (regla #11) dice "El PID corre en firmware ESP32, no en el display. Esto es no-negociable: latencia determinística, seguridad ante caída del display, eficiencia." Eso implica ESP-IDF/FreeRTOS real, no Arduino single-thread. Pero no queremos perder semanas reescribiendo NMEA2000_esp32 ni eModbus. **Tu pregunta:** ¿Apruebas opción ⭐, o prefieres una de las otras dos? ### Decisión 2.2 — Librerías nuevas a añadir a `platformio.ini` Por la regla #4 del brief ("No agregues dependencias sin preguntarme"), te las pido por adelantado: | Lib | Versión / fuente | Por qué | Licencia | |---|---|---|---| | `ttlappalainen/NMEA2000-library` | latest stable, PIO registry | Base de toda la pila NMEA 2000. Sin esto no leemos heading | MIT | | `ttlappalainen/NMEA2000_esp32` | latest stable, PIO registry | Driver CAN específico para ESP32 (TX=GPIO16, RX=GPIO4 por defecto). Incluye su propio driver CAN, no hace falta más | MIT | | `eModbus/eModbus` | v1.7.4 (Jun 2025) | Modbus RTU server (slave) con tasks FreeRTOS asíncronos, no bloquea el lazo principal | MIT | Todas MIT, todas maduras, todas mantenidas. Las añado a `platformio.ini` con versiones pinneadas (no `latest`) para reproducibilidad. **Tu pregunta:** ¿Las apruebo así o quieres otras alternativas? ### Decisión 2.3 — Asignación de núcleos y prioridades FreeRTOS ESP32-DOWD tiene 2 núcleos. Propuesta de mapeo: | Núcleo | Tarea | Prioridad | Frecuencia | |---|---|---|---| | 1 (Core 1 / APP_CPU) | `pid_inner_loop_task` (Sprint 2; stub en Sprint 1) | 24 (alta) | 50 Hz, hard real-time | | 1 | `rudder_sensor_task` (lectura AI1, mediana 5 muestras) | 23 | 100 Hz | | 1 | `safety_monitor_task` (limit switches, watchdog feed) | 22 | 50 Hz | | 0 (Core 0 / PRO_CPU) | `nmea2000_rx_task` (CAN polling, parsing PGN) | 15 | event-driven | | 0 | `modbus_slave_task` (gestiona eModbus internals) | 14 | event-driven | | 0 | `health_reporter_task` (uptime, RAM libre, IDFs varios) | 5 | 1 Hz | **Aislamiento explícito:** el lazo PID **nunca** comparte núcleo con comunicaciones. Si el bus NMEA 2000 satura, el control no se ve afectado. **Tu pregunta:** ¿Mapeo OK o quieres modificar? ### Decisión 2.4 — Estrategia de logging en firmware Para depurar sin display, necesito ver qué pasa adentro de la ESP32. Opciones: 1. **Solo `ESP_LOG` por UART0** (consola serie por USB). Estándar ESP-IDF. Cero overhead cuando se desactiva. (**Recomendado**) 2. **Log circular en RAM + dump por Modbus a petición**. Más sofisticado, útil para post-mortem en campo, pero complejidad innecesaria en Sprint 1. 3. **Log a SD card.** La tarjeta AR-NMEA-IO no tiene SD según el brief; descartado. **Mi recomendación:** opción 1 para Sprint 1. La 2 se podría añadir en Sprint 6 (Seguridad y alarmas) si te parece útil. **Tu pregunta:** ¿Opción 1? --- ## 3. Entregables del sprint ``` firmware/ar_autopilot_v1/ ├── platformio.ini # nuevo: configurado con framework + libs aprobadas ├── sdkconfig.defaults # nuevo: tuning ESP-IDF (FreeRTOS tick, etc.) ├── src/ │ ├── main.cpp # nuevo: arranca tareas FreeRTOS, entra en loop tonto │ ├── pid/ │ │ ├── pid_outer.cpp # stub: devuelve setpoint=0, log │ │ └── pid_inner.cpp # stub: devuelve PWM=0, log │ ├── modes/ │ │ └── standby.cpp # nuevo: única lógica de modo en Sprint 1 │ ├── safety/ │ │ ├── watchdog.cpp # nuevo: TWDT habilitado a 2 s │ │ └── override_handler.cpp # nuevo: DI1 → siempre fuerza STANDBY │ ├── protocols/ │ │ ├── modbus_slave.cpp # nuevo: servidor Modbus mínimo │ │ ├── modbus_registers.h # nuevo: mapa de registros expuestos │ │ └── nmea2000_consumer.cpp # nuevo: lee 127250 + 127251, loguea │ ├── hal/ │ │ ├── pinout.h # ya existe (Sprint 0) │ │ ├── rudder_sensor.cpp # nuevo: lee AI1 con filtro mediana │ │ ├── rudder_actuator.cpp # nuevo: stub que escribe DO1/DO2 pero comando=0 │ │ └── di_do.cpp # nuevo: helpers para todos los DI/DO │ └── system/ │ └── health_reporter.cpp # nuevo: log periódico de salud ├── include/ # headers públicos ├── test/ │ ├── test_modbus_registers/ # Unity: validar mapa de registros │ ├── test_filters/ # Unity: mediana de 5 muestras │ └── test_pinout/ # Unity: que los pines no se pisen └── tools/ └── modbus_client_test.py # script Python para probar el slave desde PC ``` Y **en el lado Python:** ``` arautopilot/ ├── core/ │ └── modbus_registers.py # nuevo: mismo mapa que firmware, generado / espejo ├── shared/ │ └── modbus_register_map.py # constantes single-source-of-truth para ambos lados └── tests/ └── test_modbus_register_map.py # nuevo: sanity check sobre el mapa ``` > **Importante:** el mapa de registros Modbus tiene que ser > **single-source-of-truth**. Mi propuesta: definirlo en YAML en > `firmware/ar_autopilot_v1/modbus_registers.yaml` y generar tanto > `modbus_registers.h` (C++) como `modbus_registers.py` (Python) con > un script en `tools/`. Así no se desincronizan jamás. Te lo presento > mejor cuando arranquemos. --- ## 4. Plan de fases dentro del sprint | Fase | Días | Objetivo | Demo al final | |---|---|---|---| | 1.1 | 1-2 | `platformio.ini` + esqueleto FreeRTOS + LED de vida | Tarjeta arranca, parpadea LED, log por UART0 | | 1.2 | 3-4 | HAL: lectura de los 9 inputs, escritura de los 10 outputs | Script Python por UART manda comandos manuales, se ven los LEDs/relés | | 1.3 | 5-7 | Modbus RTU slave + mapa de registros generado | Cliente Modbus desde PC lee estado, escribe setpoint dummy | | 1.4 | 8-10 | NMEA 2000 consumer: PGN 127250, 127251 | Conectada al simulador NMEA 2000 (o a tu AR-ECDIS si está), aparecen heading y ROT en los registros Modbus | | 1.5 | 11-13 | Watchdog + DI1 override + modo STANDBY explícito | DI1 pulsador funciona, watchdog reinicia si se cuelga firmware | | 1.6 | 14-15 | Tests Unity + pruebas integradas + documentación | `pio test` verde, `docs/firmware.md` con instrucciones de flash | | 1.7 | 16-20 | Buffer + integración con `arautopilot.studio.simulator` (vessel + sensor sim) | Demo end-to-end: simulador → ESP32 → cliente Modbus en PC | --- ## 5. Criterios de aceptación del Sprint 1 (Equivalente a lo que hicimos en Sprint 0) 1. ✅ `pio run` compila sin warnings en modo `release` y `debug`. 2. ✅ `pio test` pasa todos los tests Unity del firmware. 3. ✅ `pytest` sigue verde en el lado Python (80+ tests existentes + nuevos del mapa Modbus). 4. ✅ `bash scripts/dev.sh check` (lint+typecheck+test Python) verde. 5. ✅ Demo manual documentada en `docs/sprint-1-demo.md`: - Conexión por USB → terminal serie a 115200 baudios → ver logs - Cliente Modbus Python (`tools/modbus_client_test.py`) → lee estado y bits de I/O - Simulador NMEA 2000 → publica PGN 127250 con heading 045°, el firmware lo loguea y lo expone en el registro Modbus 6. ✅ Watchdog: si descomento un `vTaskDelay(10000)` en `pid_outer_loop_task`, el firmware se resetea solo y vuelve a STANDBY con alarma loggeada. 7. ✅ Tag `sprint-1` en `main`. --- ## 6. Riesgos identificados | Riesgo | Mitigación | |---|---| | **Tarjeta AR-NMEA-IO física no disponible** durante el sprint | Sprint 1 se puede empezar sin hardware: PlatformIO permite compilación + tests sin board. Los tests Unity corren en host. Las pruebas integradas se postergan hasta tener tarjeta. **Tu input:** ¿la tarjeta está físicamente disponible o trabajo "ciego" hasta tenerla? | | **GPIOs del pinout.h no coinciden con la tarjeta real** | El header del Sprint 0 dice claramente "PLACEHOLDERS aligned with the AR-NMEA-IO schematic. They will be confirmed and locked in Sprint 1 against the real board revision." Antes de Fase 1.2 te pido el esquemático o el manual de la tarjeta | | **El bus NMEA 2000 del entorno de pruebas no está disponible** | Plan B: simulador software (`tools/n2k_simulator.py`) que inyecte PGN 127250 sintéticos a la ESP32 vía un transceiver USB-CAN (Canable, Yacht Devices YDNU-02) | | **eModbus no maneja DE/RE automáticamente** | Lo verifico en Fase 1.3. Si no, escribo un wrapper de 30 líneas que use el `PIN_RS485_DE` del pinout | | **Conflicto de IRQ entre CAN driver y UART de logging** | Asignamos a núcleos separados (decisión 2.3). El log va por UART0 que es PRO_CPU; CAN driver corre interrupts en APP_CPU sin solapamiento | | **Arduino-as-ESP-IDF-component se rompe entre versiones** | Pinneamos versión exacta de `framework-arduinoespressif32` en `platformio.ini`, no `latest` | --- ## 7. Lo que necesito de ti antes de empezar 1. **Apruebas/modificas las 4 decisiones técnicas** de la sección 2. 2. **Tag de Sprint 0 aprobado:** ```powershell git tag -a sprint-0-approved -m "Sprint 0 reviewed and approved" ``` 3. **Confirmas disponibilidad del hardware** (tarjeta AR-NMEA-IO física): - Si la tienes: trabajamos contra ella desde Fase 1.2. - Si no: trabajamos en bench (todos los tests Unity + simulador) y la integración real se pospone para cuando llegue. 4. **Esquemático o pinout real** de la AR-NMEA-IO v1.0, para confirmar los GPIOs placeholder de `pinout.h`. Si está en repositorio separado, dame la ruta. Si está en papel, una foto sirve. 5. **Acceso a un bus NMEA 2000 funcional** para pruebas (o lo hacemos con un simulador software y un adaptador USB-CAN). --- ## 8. Plan de comunicación durante el sprint Mismo modelo que Sprint 0 (resultó bien): - **Inicio de cada fase**: te resumo qué voy a hacer y espero OK. - **Final de cada fase**: commit + breve resumen + demo de qué se puede tocar/ver. - **Si encuentro algo no previsto** (riesgo nuevo, decisión técnica con trade-off real), paro y te pregunto antes de improvisar. - **Tests siempre verdes antes de cerrar fase**. Si rompo algo, lo arreglo antes de pedirte review. --- ## 9. Estado actual del repo (al momento de escribir este plan) ``` $ git log --oneline 8d4a698 polish(sprint-0): clean code per ruff + mypy strict 700756c sprint-0: foundations -- data model, seed library, tests, demo ``` (Y el commit del runner dev que viene a continuación de este plan.) Working tree limpio, 80/80 tests verde, ruff limpio, mypy strict limpio. --- ## 10. Fin Cuando termines de leer: - Si todo bien: tag `sprint-0-approved` + "dale Sprint 1" → arranco por Fase 1.1. - Si hay que cambiar algo: dime qué decisión te encaja distinto y reescribo el plan. No empiezo nada hasta que coincidamos. — Claude