sprint-2: PID inner loop + Python rudder simulator
End-to-end implementation per docs/sprint-2-plan.md.
Builds: pio run -e esp32-dev SUCCESS, RAM 6.8%, Flash 26.8% (351 KB).
Tests: pytest 129/129 green (110 Sprint 1 + 19 Sprint 2).
Python (arautopilot/studio/simulator/):
- rudder_dynamics.py: marine-realistic physical model of a hydraulic
rudder actuator. Defaults tuned so 100 % PWM produces steady-state
v_max ~5 deg/s, matching the brief's "typical 3-6 dps" for a 30 m
yacht. Includes deadband, min-useful PWM snap, port/stbd asymmetry,
end-stops, optional external torque, RunRecorder helper.
- pid_inner.py: pure-Python reference PID. Anti-windup via back-
calculation, setpoint rate limit, setpoint deadband, derivative LPF,
actuator non-linearity compensation. This module is the algorithmic
source of truth; C++ firmware is a line-by-line port.
Firmware (firmware/ar_autopilot_v1/src/pid/):
- pid_inner.h: header-only C++17 controller, byte-equivalent port of
pid_inner.py. Compiles on ESP32 toolchain AND on host g++/clang/MSVC
(no Arduino dependencies) -- ready for native Unity cross-validation
once a host compiler is installed.
- pid_inner_task.{h,cpp}: FreeRTOS task wrapper. 50 Hz on Core 1
(real-time core). Subscribes to TWDT, bleeds integrator during
STANDBY, surfaces telemetry + tunables via the Modbus slave.
Modbus map (regenerated from YAML):
- 6 new INPUT registers (40-45): setpoint, output, error, kp/ki/kd live
- 4 new HOLDING registers (16-19): writable setpoint + kp/ki/kd req
(writes propagate atomically; zero kp rejected as ILLEGAL_DATA_VALUE)
Tests:
- test_rudder_simulator.py: 9 tests (zero-input rest, full deflection,
end-stop saturation, deadband, min-useful snap, asymmetry, recorder
API, invalid dt, end-stop velocity zeroing).
- test_pid_inner_python.py: 10 tests (positive/negative step response,
setpoint deadband holds, anti-windup bounds under saturation,
allowed=false bleeds integrator, actuator deadband + asymmetry
compensation, output saturation, rate limit, disturbance rejection).
NOT in Sprint 2 (intentional per brief sec. 12):
- Outer heading PID, gain scheduling by SOG, ROT feed-forward
(those land in Sprint 3)
- Cross-validation tests via ctypes (need host C++ compiler that
this Windows machine lacks; algorithmic parity enforced by review)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0-sprint2] — Sprint 2 — PID inner loop + rudder simulator — 2026-05-18
|
||||
|
||||
> Continues the overnight execution under blanket authorisation. Builds on
|
||||
> Sprint 1 firmware foundation. New cross-cutting concern introduced:
|
||||
> Python is the **algorithmic source of truth** for the PID; C++ firmware
|
||||
> is a line-by-line port; tests pin both.
|
||||
|
||||
### Added
|
||||
|
||||
#### Python (`arautopilot/studio/simulator/`)
|
||||
|
||||
- **`rudder_dynamics.py`** -- bench-grade physical model of a hydraulic
|
||||
rudder actuator. Marine-realistic defaults (actuator_gain=0.2, friction=4
|
||||
-> steady-state v_max ~5 deg/s for a 30 m yacht). Includes deadband,
|
||||
min-useful-PWM snap, port/starboard asymmetry, mechanical end-stops,
|
||||
optional constant external torque, and a `RunRecorder` helper for
|
||||
trajectory capture.
|
||||
- **`pid_inner.py`** -- pure-Python reference implementation of the inner
|
||||
PID. Anti-windup via back-calculation, setpoint rate limit, setpoint
|
||||
deadband, derivative low-pass filter, actuator non-linearity
|
||||
compensation (deadband + min-useful + asymmetry). Algorithmic source
|
||||
of truth -- the firmware C++ port matches it line-by-line.
|
||||
|
||||
#### Firmware (`firmware/ar_autopilot_v1/src/pid/`)
|
||||
|
||||
- **`pid_inner.h`** -- header-only C++17 controller, byte-equivalent port
|
||||
of `pid_inner.py`. Compiles on the ESP32 toolchain AND on host
|
||||
g++/clang/MSVC (no Arduino dependencies). Suitable for native Unity
|
||||
tests once a host compiler is available.
|
||||
- **`pid_inner_task.{h,cpp}`** -- FreeRTOS task wrapper. 50 Hz on Core 1
|
||||
(real-time core). Reads rudder position from `hal::rudder_sensor`,
|
||||
consumes setpoint from Modbus / outer loop, commands
|
||||
`hal::rudder_command`. Bleeds integrator during STANDBY. Subscribes
|
||||
to the watchdog and feeds it every loop.
|
||||
|
||||
#### Modbus register map (regenerated from YAML)
|
||||
|
||||
- 6 new INPUT registers (40-45) -- PID telemetry: setpoint, output,
|
||||
error, kp/ki/kd live.
|
||||
- 4 new HOLDING registers (16-19) -- writable: rudder setpoint request,
|
||||
kp/ki/kd request. Writes propagate atomically to the controller.
|
||||
|
||||
#### Tests
|
||||
|
||||
- `test_rudder_simulator.py` -- 9 tests of the physical model: zero-input
|
||||
rest, full-deflection drive, end-stop saturation, deadband, min-useful
|
||||
snap, asymmetry behaviour, recorder API, invalid dt, end-stop velocity
|
||||
zeroing.
|
||||
- `test_pid_inner_python.py` -- 10 tests of the Python PID against the
|
||||
simulator: positive/negative step response, setpoint deadband holds,
|
||||
anti-windup bounds integrator under saturation, allowed=false bleeds
|
||||
integrator, actuator deadband compensation, asymmetry compensation,
|
||||
output saturation, rate limit caps slew, disturbance rejection.
|
||||
|
||||
### Verification
|
||||
|
||||
- `pio run -e esp32-dev` -- SUCCESS, RAM 6.8 %, Flash 26.8 % (351 KB).
|
||||
- `pytest` -- **129 passed** in 0.31 s (110 Sprint 1 + 19 Sprint 2 new).
|
||||
|
||||
### Not in Sprint 2 (intentional)
|
||||
|
||||
- Heading control (outer loop) -- that is Sprint 3.
|
||||
- Gain scheduling by SOG -- Sprint 3.
|
||||
- Rate-of-Turn feed-forward -- Sprint 3.
|
||||
- Cross-validation tests Python ↔ C++ via ctypes -- requires host C++
|
||||
compiler that this machine lacks; the algorithm parity is enforced by
|
||||
code review (line-by-line port) and will be backed by automated
|
||||
cross-validation as soon as a host compiler is available.
|
||||
|
||||
## [0.1.0-sprint1] — Sprint 1 — Firmware ESP32 base — 2026-05-18
|
||||
|
||||
> Sprint 1 was executed autonomously overnight after the user gave explicit
|
||||
|
||||
Reference in New Issue
Block a user