dotbot: expand each LH2 fix into a body pose and draw the robot from it - #297
Merged
Merged
Conversation
AI-assisted: Claude Opus 5
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
AI-assisted: Claude Opus 5
AI-assisted: Claude Opus 5
AI-assisted: Claude Opus 5
AI-assisted: Claude Opus 5
AI-assisted: Claude Opus 5
AI-assisted: Claude Opus 5
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
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)
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
AI-assisted: Claude Opus 5
AI-assisted: Claude Opus 5
AI-assisted: Claude Opus 5 (1M context)
AI-assisted: Claude Opus 5 (1M context)
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
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
…tprint 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
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
Codecov Report❌ Patch coverage is Additional details and impacted files@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
AI-assisted: Claude Opus 5.5
…4 px AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
…nown 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
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
…owser 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
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
…s first fix AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
…l toggles AI-assisted: Claude Opus 5.5
… settles 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
…ys sends AI-assisted: Claude Opus 5.5
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.
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 Cgeometry.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
dotbot/robots.pybecomes 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.pynow 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.RobotGeometry.body_pose(sensor, heading, source)turns the photodiode fix plus a heading into aBodyPose: photodiode, axle, centre, nose, LED, outline and wheel rectangles, in the arena frame. The controller computes it where it stores the position, andDotBotModelgainsmodelandposebeside the unchangedlh2_position(still literally the sensor), so REST, the WebSocket stream and the CSV log carry both.heading_source(none/travel/ekf); with no heading the pose faces a placeholder heading flaggednone, 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.GET /controller/device_poses, serves a headingless pose at the origin for each swarmit device type the geometry record covers (DotBotV3maps todotbot-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.BotGlyph.tsxholds 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.0xFFFF, which never matches; it now checks the-1000sentinel. The debug log printedY=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
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.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.DotBotWaypointsdocstring now says so.modelis always the defaultdotbot-v3for robots heard through the controller: no per-bot revision is carried yet, andbody_poseon an unknown model raises rather than guessing. The device-type mapping inSWARMIT_DEVICE_MODELSis 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_posefrom about 126 to 77 us); a test pinsSWARMIT_DEVICE_MODELSto swarmit'sDeviceTypenames; 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
Validation
pytest dotbot/tests: 843 pass; the only failures are the known environmental ones on the dev machine (test_cli_helperstests reading the local~/.dotbot/config.toml, andtest_controller_dotbot_simulatorwith its port taken), which fail the same way onmain.npm testinconsole-web: 573 passed;npm run buildandnpm run lintexit 0.pre-commit run --all-filesis clean.travel.travelafter 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
doc/reference/text for the waypoint contract and the new route) comes after review.Plan