sprint-3: PID outer + Heading Hold + ROT feed-forward + gain scheduling
End-to-end implementation per docs/sprint-3-plan.md.
Closes the cascade: outer loop (heading control, 10 Hz on Core 1) drives
the inner loop (rudder position control, 50 Hz from Sprint 2). First real
mode other than STANDBY is now activable: HEADING_HOLD.
Builds: pio run -e esp32-dev SUCCESS, RAM 6.8%, Flash 27.1% (355 KB).
Tests: pytest 258/258 green (231 Sprint 2.5 + 27 Sprint 3 new).
Python (arautopilot/studio/simulator/):
- vessel_heading.py: first-order yaw model. ROT responds to
rudder*speed; damping returns ROT to zero when rudder is centred.
Defaults tuned so 5 deg rudder @ 10 kn -> ~3 dps steady-state ROT.
Includes heading_error_deg() shortest-arc helper.
- pid_outer.py: pure-Python outer heading PID. Anti-windup via back-
calculation, gain scheduling by SOG, deadband, derivative LPF,
output saturation, ROT feed-forward (brief sec. 6 -- the term that
distinguishes a premium autopilot from a basic one), rate limit on
produced rudder setpoint, shortest-arc heading wrap-around.
Firmware (firmware/ar_autopilot_v1/src/pid/):
- pid_outer.h: header-only C++17 port. Same algorithm, same variables,
same numerics. Fixed-capacity gain schedule (up to 8 points).
- pid_outer_task.{h,cpp}: 10 Hz FreeRTOS task on Core 1. Subscribes to
TWDT. Reads heading + ROT from the NMEA 2000 snapshot. Uses
operator-configurable SOG (default 15 kn until PGN 129026 wiring in
Sprint 5). Pushes rudder setpoint into the inner loop only when
current_mode == HEADING_HOLD.
Modes (firmware/ar_autopilot_v1/src/modes/standby.cpp):
- HEADING_HOLD activable via request_mode(). Pre-conditions:
* NMEA 2000 heading sensor valid (fresh PGN 127250)
* Rudder sensor valid (median filter filled)
On success, captures current heading as initial setpoint so the
operator doesn't get a sudden swing toward an old setpoint.
Modbus (regenerated from YAML):
- 7 new INPUTs (50-56): outer heading setpoint, produced rudder
setpoint, error, current SOG, live kp/ki/kd.
- 5 new HOLDINGs (24-28): writable heading setpoint, SOG override,
outer base gains. Writing any of kp/ki/kd disables the built-in
3-point gain schedule (operator override).
Tests:
- test_vessel_heading_simulator.py: 6 dynamics tests + 9 parameterised
heading_error_deg edge cases (wrap-around).
- test_pid_outer_python.py: 12 tests covering gain interpolation,
per-tick PID behaviour (deadband, sign, ROT feed-forward,
saturation, rate limit, allowed=false), and three end-to-end cascade
tests (positive step, negative step, wrap-around 360->10).
Cascade verification: outer + inner + rudder dynamics + vessel-heading
simulator settles a 30 deg step within +-2 deg in 60 s.
NOT in Sprint 3 (intentional):
- True Course / Track Keeping / Dodge -- Sprint 5
- Off-course alarms + auto-disengage on sensor loss -- Sprint 6
- COG / SOG / Position via N2K PGN 129025/9/6 -- Sprint 5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user