The filter
eskf.cpp · 739 lines, no dependencies| Error state | Dim | Units | Bounded by |
|---|---|---|---|
| δp | 3 | m | GNSS position · baro (vertical) |
| δv | 3 | m/s | GNSS velocity · baro damping |
| δθ | 3 | rad | gravity when still · mag heading |
| δbg | 3 | rad/s | averaged in the alignment window |
| δba | 3 | m/s² | observable once anchored |
| δbb | 1 | m | GNSS-vs-baro altitude, weather drift |
The attitude error lives in the world frame, not the body.
That one choice makes heading error exactly the down component of
δθ — so the magnetometer structurally cannot corrupt roll or pitch —
and it makes the transition matrix exact: F is nilpotent, and
Φ = I + Fdt + F²dt²/2 + F³dt³/6 is the full exponential,
not a truncation.
Every update is Joseph-form with a χ² gate at 99%, and the baro noise model inflates with speed² and through the transonic — driven by the estimated state, never by flight-phase logic.
Altitude never waits for GNSS. The baro-damped vertical channel runs from the end of alignment, indoors or out; a late fix upgrades to full navigation with the altitude kept continuous.
Flight sequence
FiltState — the enum, verbatimInit
Power on. The first IMU sample arms the filter and starts the clock.
Align
2.5 s of measured stillness. Gravity gives tilt, the mag gives heading, and the averaged gyro rate is the calibration.
Wait fix
Attitude and the baro vertical channel run. The first valid GNSS fix anchors the world origin.
Run
Full 3-D navigation: position, velocity, attitude, and three bias estimates, output at 50 Hz.
Att-only
No fix in 20 s? Fly on attitude + baro altitude. Nothing blocks on the sky.
The branch heals itself: a fix arriving in FS_4 anchors late and upgrades 4 → 3, rebuilding position and re-seeding the baro bias so altitude stays continuous — a late anchor is identical to an on-time one.
Ground station
viewer/index.html · one file, no librariesUSB or LoRa
Pick USB for the rocket's 50 Hz stream or LoRa for the ESP32-C3 base station's 4 Hz bridge — same records, same page. Line-buffered parsing resynchronizes mid-stream.
Live plots
Altitude, rates, biases, innovations and χ² NIS with rejection marks — the filter argues its own health.
3-D attitude
Hand-rolled renderer, orbit camera, your own STL decimated to a closed surface — no wireframes.
Pad-anchored map
Satellite tiles with metric range rings, live track, distance and bearing from the pad.
Session capture
Pause, scroll back, export CSV or raw JSONL. Disconnect and reconnect without a reload.
Visible degradation
Stale badges and dashes, never invented numbers. If a sensor dies, the page says so.
Verification
test/eskf_test.cpp · viewer/parse_test.js| Item | Method | Result | Call |
|---|---|---|---|
| Filter, six scenarios | Native host build, truth simulation: static, mag-off, GPS-less, nose-up gimbal lock, 400 m GPS-less climb | 91 / 91 | GO |
| Baro latency model | 40 m/s steady climb; innovation mean with compensation | 0.06 m vs 0.80 m | GO |
| Late-anchor continuity | Anchor at 400 m AGL; baro acceptance after bias re-seed | 50 / 50 accepted | GO |
| Stream parser | Replay file with torn, malformed and future records | 41 / 41 | GO |
| Firmware build | arduino-cli, GENERIC_F722RETX | 145 612 B / 20 072 B | GO |
| LoRa link | Bench, 433.5 MHz SF7 / 500 kHz: rocket Ra-02 ⇄ ESP32-C3 base station, 4 Hz downlink with command uplink | 0 % loss · 6 / 6 uplinks | GO |
| First flight | — | awaiting vehicle | HOLD |
verified