Skip to content

dotbot: expand each LH2 fix into a body pose and draw the robot from it - #297

Merged
geonnave merged 56 commits into
DotBots:mainfrom
geonnave:pose-geometry
Sep 23, 2026
Merged

geonnave merged 56 commits into
DotBots:mainfrom
geonnave:pose-geometry

Conversation

@geonnave

@geonnave geonnave commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Every position the testbed handles is the LH2 photodiode, and nothing turned it into where the robot's body is. The console centred the board outline on the sensor, so every DotBot v3 was drawn 29 mm too far forward (the photodiode sits 29.0 mm ahead of the outline centre and 53.5 mm ahead of the axle midpoint). The camera overlay, drawn from the detector's own body pose, landed in the right place, so the two layers disagreed on screen by exactly that offset. On top of that, the v3 geometry existed as four hand-copied forms (robots.py, the C geometry.h, the camera detector, BotGlyph.tsx), only one pair guarded by a test, and the detector already carried a second track width (85 mm against the drivetrain's 78).

Approach

  • One geometry record. dotbot/robots.py becomes the complete per-revision record in the KiCad board frame: outline path, photodiode, LED, caster, axle midpoint, track, wheel, tyre, encoder and gear. Every offset (lever arm, edge distances, reach, core, envelope) is a derived property, not a typed number. The camera detector and the simulator read it and their copies are deleted; test_control_loop_geometry.py now pins the lever arm, lever angle and track against the compiled C control loop. The detector's 85 mm track became the record's 78 with no loss of template score on the fixtures.
  • One expansion, in the controller. RobotGeometry.body_pose(sensor, heading, source) turns the photodiode fix plus a heading into a BodyPose: photodiode, axle, centre, nose, LED, outline and wheel rectangles, in the arena frame. The controller computes it where it stores the position, and DotBotModel gains model and pose beside the unchanged lh2_position (still literally the sensor), so REST, the WebSocket stream and the CSV log carry both.
  • The heading says where it came from. The only heading on the wire today is the travel bearing between fixes, which is stale at rest and backwards when reversing. The pose carries heading_source (none / travel / ekf); with no heading the pose faces a placeholder heading flagged none, and the console draws no guessed body for it. A later change appends a source byte to the advertisement once the on-bot estimator exists; nothing here changes the firmware or the wire.
  • A pose for robots outside their app. A robot in the swarmit bootloader runs no app, so it computes no heading and the controller hears nothing from it; only swarmit's STATUS position and device type are known. A new host route, GET /controller/device_poses, serves a headingless pose at the origin for each swarmit device type the geometry record covers (DotBotV3 maps to dotbot-v3). The console moves that pose onto swarmit's position, so a bootloader robot is sized from its device type and drawn as its sensor mark plus the possible footprint. A device type the host has no record of stays a bare point.
  • The console draws from the pose. BotGlyph.tsx holds no geometry any more: board, wheels and photodiode all come from the pose. The body is filled with the robot's state colour at every zoom level. A known heading is shown as a white bar; there is no separate heading line at detail zoom and no drive dot. The LED colour is one sensor mark at the photodiode: filled with the commanded colour, near-black when commanded off, and hollow when unknown. The colour counts as known only while the app runs, since leaving the app resets the LED. When the robot is too small on screen for a body, it is drawn as a small circle with the heading bar across it; a sensor point with a known heading carries the same bar. The Layers tab's "Robot shapes" toggle becomes a Body | Sensor choice with a "Possible footprint" ring (reach and core radii about the photodiode, valid for an unknown heading), cased so it stays readable over the camera image. Waypoint markers are sized from the robot's body, capped at 14 px. The camera overlay also draws the tyres, now published by the camera pose from the same record.
  • Wire fixes. The controller skipped a no-heading frame by comparing the signed direction to 0xFFFF, which never matches; it now checks the -1000 sentinel. The debug log printed Y=pos_x.

The scope grew past the pose itself during the work, and those pieces ride on this branch: the simulator gains a bot with no heading (it holds its heading back until 50 mm of travel, as the firmware does) and its built-in waypoint loop now steers on the true angle, which fixes a bot that could spin forever facing away from its waypoint; the side-panel toggles are wider, bound to [ and ], and each panel's collapsed state is remembered per browser; the map now stays still when a side panel opens or closes (the zoom was tied to the canvas width, so in a narrow window a toggle moved robots by up to 337 px), and the saved map view is stored as its centre point and scale rather than tied to the canvas size; the site view is framed tighter (a tenth of the site's longer side, floored at 250 mm) so robots show full outlines at the site zoom, with a separate 2 m pan allowance past the site; the minimap centres on the point pressed.

What a reviewer should check

  • The heading convention in body_pose: forward is (-sin, cos) and body-left (cos, sin), matching the firmware's 0 = +y, clockwise-positive heading. A test checks the outline against the camera detector's point for point at five headings.
  • The placeholder pose for heading_source: "none" is a deliberate choice (the API always carries a pose beside a valid fix; the flag says the orientation is unknown). Consumers that draw must honour the flag, as the console does.
  • Waypoints still target the photodiode, the point the firmware steers. The DotBotWaypoints docstring now says so.
  • model is always the default dotbot-v3 for robots heard through the controller: no per-bot revision is carried yet, and body_pose on an unknown model raises rather than guessing. The device-type mapping in SWARMIT_DEVICE_MODELS is the one place a swarmit device type is tied to a geometry record.

Cross-repo

Goes with DotBots/swarmit#166, which parses the STATUS position as unsigned (the device sends two uint32). The two are independent at runtime and can merge in either order.

Review round

A code review of this branch, with each finding checked by a second reviewer told to refute it, led to these fixes. Console: a side-panel toggle now keeps every floor point where it was on screen, not only the canvas centre (the rail and the right pane moved robots by 144 px and 136 px in the review's measurement); calibration started with the right pane collapsed now fits the session to the canvas the pane leaves, and on Done restores the previous view through its centre and scale and collapses the pane again, without remembering the forced expand; a restored view is held under the zoom ceiling, and "+" never zooms out from above it; the joystick reads its heading from the pose; the panel keys ignore key auto-repeat; the pose's wheels and radii are required in the TypeScript type, as the host always sends them. Host: a frame with no heading now clears the stored direction (None) rather than keeping a stale one; the controller's kinematic twin is seeded with its first fix's heading and heading origin; the heading-invariant geometry is cached (body_pose from about 126 to 77 us); a test pins SWARMIT_DEVICE_MODELS to swarmit's DeviceType names; one warped bench frame (36 KB JPEG) now pins the detector's status, centre and heading on a real photograph. Two tests that could not fail or restated the code were dropped, and some comments were corrected. The formatting pass (black, isort, pyupgrade) is done.

Deferred: an accessibility pass on the new controls, memoising per-frame glyph geometry, treating a (0, 0) fix as no fix, and the pre-existing full model rebuild on every update.

Size

+ - Files
Tests (Python and vitest) +2506 -351 31
Console source (TS, TSX, CSS) +1286 -381 22
Python source +505 -124 10
Simulator world files +18 -0 3
Binary test fixture (one 36 KB JPEG) 1
Generated +0 -0 0
Total +4315 -856 67

Validation

  • pytest dotbot/tests: 843 pass; the only failures are the known environmental ones on the dev machine (test_cli_helpers tests reading the local ~/.dotbot/config.toml, and test_controller_dotbot_simulator with its port taken), which fail the same way on main. npm test in console-web: 573 passed; npm run build and npm run lint exit 0. pre-commit run --all-files is clean.
  • Bench, 2026-09-22, one DotBot v3 under the overhead camera: across 1376 advertisements the pose centre is exactly 29.0 mm behind the fix along the pose's heading. At rest after a straight run, the pose centre is 20.3 mm from the camera's body centre, the same as the raw LH2 fix against the camera's photodiode (20.3 mm), so the expansion adds nothing to the existing LH2-versus-camera offset; the travel heading read -40.0 deg against the camera's -39.7. Reversing segments showed the travel bearing pointing backwards, as expected, flagged travel.
  • Bench, 2026-09-23, two DotBot v3 robots under the overhead camera, including one taken into the swarmit bootloader and back. This predates the review round, whose panel-toggle and calibration-camera fixes are checked by unit tests only.
  • Simulator, headless: the no-heading bot starts as a sensor point, switches to travel after its first 50 mm and draws a body; bots reach waypoints behind them in about 3 s. Before and after screenshots taken, light and dark.

Known follow-ups

  • The bench check that the camera outline and the LH2-driven outline overlap at rest in the console is still to be done by the developer, as is a bench look at the review-round console fixes.
  • The docs sweep (doc/reference/ text for the waypoint contract and the new route) comes after review.

Plan

The template's tyres move from an 85 mm track and 18 mm tyre to the
record's 78 and 17.5. On the synthetic fixtures the nose margin is
unchanged (mean 2.244 before, 2.241 after, six headings)

AI-assisted: Claude Opus 5
… sends

The board was drawn centred on the photodiode, 29 mm from where the robot
stands. A pose flagged heading_source none now draws only the sensor dot,
where a headingless DotBot used to get a nose-up board: a guessed
orientation is worse than none.

AI-assisted: Claude Opus 5
The camera detector builds its own tyre from `TRACK_MM` and `TYRE_W_MM` off
the same record but with a hardcoded 40 mm depth, and its `frame_pose`
publishes no wheels at all, so the detection overlay and the robot glyph will
not draw the same tyre until both read `wheel_paths`.

AI-assisted: Claude Opus 5 (1M context)
The template drew a 40 mm tyre where the record and the firmware's
DB_WHEEL_DIAMETER both say 44, so the rasterised tyre region grows by that
much. On synthetic frames it costs about 1% of the template margin, which
sits four times over its floor, and leaves the fitted centre and heading
unchanged.

AI-assisted: Claude Opus 5
The advertised direction only moves after 50 mm of travel, so a bot
turning in place toward a waypoint never saw its heading change and
spun forever. The custom loop keeps the advertised direction, since it
reproduces the firmware.

AI-assisted: Claude Opus 5.5
A 2 m margin on every side put a 2 x 2 m arena at about 0.115 px/mm on
a 1600x950 window, under the size the board outline reads at, so the
whole-site view only ever showed marks.

AI-assisted: Claude Opus 5.5
@codecov

codecov Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.40344% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.65%. Comparing base (8813b4b) to head (56e2637).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
dotbot/tests/test_control_loop_geometry.py 41.66% 14 Missing ⚠️
dotbot/console-web/src/api.ts 0.00% 9 Missing ⚠️
dotbot/console-web/src/useFleet.ts 81.39% 8 Missing ⚠️
dotbot/console-web/src/RightPane.tsx 93.33% 5 Missing ⚠️
dotbot/console-web/src/Inspector.tsx 57.14% 3 Missing ⚠️
dotbot/console-web/src/TestbedRail.tsx 66.66% 3 Missing ⚠️
dotbot/console-web/src/App.tsx 97.82% 2 Missing ⚠️
dotbot/console-web/src/Joystick.tsx 66.66% 1 Missing ⚠️
dotbot/console-web/src/MapView.tsx 98.96% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #297      +/-   ##
==========================================
+ Coverage   83.72%   84.65%   +0.92%     
==========================================
  Files         201      204       +3     
  Lines       24369    25336     +967     
  Branches     1683     1822     +139     
==========================================
+ Hits        20404    21449    +1045     
+ Misses       3958     3880      -78     
  Partials        7        7              
Flag Coverage Δ
console 75.59% <95.91%> (+2.08%) ⬆️
frontend 97.80% <ø> (ø)
python 86.06% <97.17%> (+0.89%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
dotbot/camera/detection/pose.py 93.12% <100.00%> (ø)
dotbot/camera/detection/propose.py 95.12% <100.00%> (+0.02%) ⬆️
dotbot/camera/detection/robot.py 98.90% <100.00%> (+0.01%) ⬆️
dotbot/console-web/src/BotGlyph.tsx 100.00% <100.00%> (ø)
dotbot/console-web/src/CalibrationLayer.tsx 66.33% <100.00%> (+3.90%) ⬆️
dotbot/console-web/src/Minimap.tsx 100.00% <100.00%> (+11.22%) ⬆️
dotbot/console-web/src/PanelToggle.tsx 100.00% <100.00%> (ø)
dotbot/console-web/src/frame.ts 100.00% <100.00%> (ø)
dotbot/console-web/src/panels.ts 100.00% <100.00%> (ø)
dotbot/console-web/src/robotDrawing.ts 100.00% <100.00%> (ø)
... and 26 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@geonnave
geonnave merged commit b51067c into DotBots:main Sep 23, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant