drv/pose_estimator: add an EKF on odometry and position-only LH2 fixes - #33
Merged
Merged
Conversation
This was referenced Sep 24, 2026
Merged
geonnave
marked this pull request as ready for review
September 25, 2026 07:24
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
… floor AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
The chain's fixes are fix_age ticks old, but its odometry ran from their arrival, so a seeded pose was the one at capture: 6 mm behind at 300 mm/s, 5.6 deg behind in a 200 mm/s spin. The odometry ring now records every predict, the chain consumes steps as they leave the age window, and the seeded pose is carried forward over that window. AI-assisted: Claude Opus 5.5
…meout AI-assisted: Claude Opus 5.5
After a 500 to 700 mm/s sprint stops hard, the wheels slip or skid, so the estimate sits 25 to 45 mm off steady fixes while the wheels stand, and the kidnap rule fired. Process noise now also grows with wheel speed changes, and a kidnap needs the wheels to have stood before the fixes jumped; a small jump soon after driving re-anchors the position and keeps the heading. AI-assisted: Claude Opus 5.5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sits directly on
mainnow that #32 has merged, so the diff below is this PR alone. The app side is DotBots/DotBot-firmware#426. The single-target steering is stacked on this one as #34.Adds
drv/pose_estimator, an extended Kalman filter on[x, y, heading]of the wheel-axle midpoint, fed by wheel odometry and LH2 fixes, plus the effective-track model it and the wheel loop's twist mixing now share. No hardware calls, so it builds on the host for its tests.Design
|d_right - d_left|, since slip is dominated by turning. Above a reference wheel-speed difference (250 mm/s, a spin at 125 mm/s per wheel) the turning term scales up in proportion.DB_LH2_LEVER_ARM), and that offset is what makes heading observable when the robot turns in place. The caller feeds each fix sequence once; a fix applied twice would shrink the covariance with no new information.P = (I - K H) P (I - K H)^T + K R K^T. On a robot at rest the fixes shrink P toward rank 1, and the plainP - K H Pupdate let it go indefinite in float32 (25 of 36,000 updates over a simulated hour at rest).DB_POSE_ESTIMATOR_KIDNAP_FIXES, 0.3 s at 10 Hz) that agree within the 20 mm seed tolerance, with at most 2 mm of summed wheel travel (DB_POSE_ESTIMATOR_KIDNAP_STILL_MM,|d_left| + |d_right|) since the first of them, count as a kidnap: the estimator returns to SEEDING at once with those fixes as the start of its seed chain. Heading stays unknown (the advert sends -1000 and falls back to the raw solve for position) until motion re-acquires it through the usual chain. Rejected fixes while the wheels turn keep the old path: hold the pose until the timeout, then reseed. Akidnapscounter joins the others for the debugger.kidnap_fixes = 0disables it.kidnap_settle_ticks). Consistent rejected fixes that arrive sooner after driving, within 60 mm of the estimate (reanchor_mm), re-anchor instead: the position moves onto the fix, the heading is kept and its variance raised by (5 deg)^2, and areanchorscounter records it. A larger jump right after driving is still a kidnap. Zero values keep the old behaviour.Passive initial-heading acquisition
The estimator starts in SEEDING with no pose and needs no calibration manoeuvre. It builds a chain of fixes and, alongside it, the axle travel and rotation odometry saw since the chain's first fix, in that fix's body frame. A fix joins the chain when its distance from the first fix agrees with the photodiode travel odometry predicts to within 20 mm, otherwise the chain restarts. Once the chain holds 3 consistent fixes and the photodiode has moved at least 40 mm in the body frame, the heading is solved from the chain and the pose is seeded, with a heading variance from the fix noise over that baseline plus what odometry added along the chain. Driving normally, or spinning in place thanks to the lever arm, is enough.
Radius-dependent effective track
On carpet the robot rotates less than the 78 mm geometric track predicts.
db_track_effective_mm(left, right)returns 81 mm for a spin in place, rising linearly with|left + right| / |right - left|to 85 mm for arcs of 100 mm radius and wider, and held there. Only the ratio matters, so it takes speeds or distances. The estimator's predict anddb_wheel_control_from_twist()both use it; the twist mixing resolves the track/radius dependency with a short fixed-point iteration. Spin speed is not modelled (the in-place figure reads 83 at 200 mm/s per wheel and 87 at 300). v1/v2 fall back to theirDB_TRACK, so nothing changes for them.Validation and status
make test): 106 pose-estimator checks (including sprints at 500 and 600 mm/s, forward and reverse, stopping with 30 to 45 mm of odometry over-count and up to 25 mm sideways that keep tracking, keep the heading and sit on the fixes 0.5 s later; the same stop re-anchoring with the slip noise off, and reading as a kidnap with the guard off too; a 150 mm move right after driving still a kidnap; a hand-move with the wheels still reseeding within 3 fixes, outliers and scattered rejected fixes while still not reseeding, rejected fixes while driving and a carry longer than the timeout both taking the timeout path, a wild fix breaking a seed chain, the seeded pose matching the present one at speed, and P staying symmetric positive definite over an hour at rest) and 154 wheel-control checks (the 150 that drv/wheel_control: add a per-wheel speed loop, brake to zero, and correct the v3 distance per count #32 brought tomain, whose suite runs on two gain sets, plus 2 per set for the effective-track mixing), all passing.make check-formatclean with clang-format 18.host-tests,style,doc).drv/pose_estimator.hnaming what they were sized from and what measurement would settle them.tests/test_pose_estimator.cdrv/pose_estimator/pose_estimator.cdrv/pose_estimator.hdrv/geometry.hMakefile,doc/sphinx/drv.md,drv/drv.emProject,drv/wheel_control.h,drv/wheel_control/wheel_control.c,tests/test_wheel_control.cMerge chain
Merged on 2026-09-25: #32, DotBots/PyDotBot#298 and DotBots/DotBot-firmware#425. The rest go one PR at a time, in this order: this PR, DotBots/DotBot-firmware#426, #34, DotBots/DotBot-firmware#427, #35, DotBots/DotBot-firmware#428, DotBots/PyDotBot#301, DotBots/PyDotBot#302, #36, DotBots/DotBot-firmware#429, DotBots/PyDotBot#303. Next to merge overall: this PR. Next after this one: DotBots/DotBot-firmware#426. Before each firmware PR merges, its
dotbot-libssubmodule is re-bumped to the DotBot-libsmaincommit its libs PR lands as, as DotBots/DotBot-firmware#425 was re-bumped to caa788b after #32.