# Sprint 3 — PID outer loop + Heading Hold

> Brief reference: §6 (cascaded PID), §12 Sprint 3.

## Objetivo

Cerrar el lazo externo de **rumbo**: dado un rumbo deseado, calcular un
setpoint de timón que el lazo interno (Sprint 2) lleva a hueso. Esto
habilita el primer modo real distinto a STANDBY: **HEADING_HOLD**.

## Estrategia

Misma topología que Sprint 2 (Python source-of-truth + C++ port):

1. **`vessel_heading.py`** — simulador del barco: yaw inertia, rate of
   turn (ROT), wash de timón (mayor velocidad sobre el agua = más yaw
   por grado de timón). Permite probar el PID outer sin barco real.
2. **`pid_outer.py`** — lazo externo. Inputs: rumbo deseado, rumbo
   actual, ROT actual, SOG. Output: setpoint de timón en grados que se
   pasa al PID inner. Incluye:
   - **Feed-forward de ROT**: anticipa cuándo dejar de aplicar timón
     para que el barco no sobrepase por inercia (brief §6).
   - **Gain scheduling por SOG**: interpola ganancias entre puntos de
     `GainSchedulePoint` (modelo de datos Sprint 0).
   - **Wrap-around de rumbo**: el error entre 358° y 2° es +4°, no
     -356°.
   - **Anti-windup + rate limit** del setpoint de timón producido.
3. **`pid_outer.h`** — port C++ header-only, byte-equivalente.
4. **`pid_outer_task.cpp`** — tarea FreeRTOS @ 10 Hz en Core 1. Lee
   rumbo de `nmea2000_consumer` (PGN 127250) + ROT (PGN 127251) + SOG
   (Sprint 1 todavía no la tenemos por PGN; Sprint 5 traerá COG/SOG vía
   PGN 129026; mientras tanto SOG=15 nudos hardcoded como default).
   Output: invoca `pid_inner_set_setpoint_deg(rudder_setpoint)`.
5. **Activación de HEADING_HOLD en `modes.cpp`**: `request_mode(HEADING_HOLD)`
   ahora aceptada **si**:
   - Sensor de rumbo válido (heading_valid == true)
   - Sensor de timón válido
   - Master power activado
6. **Captura del heading actual como setpoint inicial al engage**: cuando
   el operador hace engage en HH, el setpoint = rumbo actual, no el
   último valor escrito por Modbus.
7. **Tests Python** + cascada simulada inner+outer end-to-end (un barco
   virtual + PID outer + PID inner + simulador timón).

## Lo que NO hace Sprint 3

- True Course mode (compensación de deriva con COG). Sprint 5.
- Track Keeping. Sprint 5.
- Dodge. Sprint 5.
- Alarmas off-course. Sprint 6.
- Auto-disengage por pérdida de sensor. Sprint 6.

## Verificación

- `pytest` verde (objetivo: 250+)
- Cascada Python inner+outer estabiliza el rumbo dentro de ±2° en <30s
- `pio run -e esp32-dev` compila clean
- Modbus expone: heading_setpoint (RW), rudder_setpoint_from_outer
  (read), outer gains live, current SOG used
