Skip to content

dotbot/calibration: place a spin calibration on a hand-made site - #326

Open
geonnave wants to merge 51 commits into
DotBots:mainfrom
geonnave:site-editor-p6
Open

geonnave wants to merge 51 commits into
DotBots:mainfrom
geonnave:site-editor-p6

Conversation

@geonnave

@geonnave geonnave commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #315 (the site editor) and #343 (the top of the LH2 multi-station stack: #322, #323, #324, #325, #331, #343), joined by one merge commit at the bottom of this branch. Merge order: #315 and the LH2 stack, in either order, then this PR; #327 to #330 sit on this one. Until they land, the diff also carries their commits and shrinks to this PR's own as they merge.

A spin calibration (calibrate-lh2 collect --spin) defines its own frame, metric through the spin radius, and site init --from-calibration builds a starter site around it. This lets the operator draw the site by hand instead and drop the calibration onto it afterwards, in the site editor.

The placement (dotbot/calibration/placement.py) is one rigid move for the whole calibration: Rigid2D(dx_mm, dy_mm, theta_deg) turns about the frame's zero, then shifts. place_calibration(calibration, site, transform) applies T . H to every station's homography (normalised), moves placement points with it, copies tracks (camera counts) and links unchanged, and returns a new Calibration in the site, with the site's name and anchor (which a controller checks a calibration against) and the site's extent as its fence. Scale is never touched, so stations keep their geometry relative to one another. It refuses:

  • a corner-collected calibration (its frame is the site's anchor, so moving it would misplace every robot), unless the caller asks to re-anchor;
  • a move that puts a spin centre, a station's rectangle (its own, else the calibration's fence), or a placement's point below zero, since robots carry positions as unsigned millimetres, and a fence a robot could not store.

Free mode is told from the stations' solver (conics-free, or conics-joint for multi-station files), not from the anchor: a placed calibration takes its site's anchor and must stay movable.

Multi-station calibrations (schema 4, from the LH2 stack underneath) carry one validity rectangle per station. On a site with an extent, every station's rectangle becomes the site's extent, as a spin solve placed on a site already does in #331; on a site without one, a station's own rectangle moves with the calibration as the box round its turned corners (rounded outwards, so it only grows). The fence is the union of the station rectangles. The placed calibration records the source's id as placed_from (#343), so collect --spin --append can name the calibration to append to. The overlay draws each station's rectangle and spin circles in its own colour, labelled "station i (channel i+1)", and the links between stations, a weak one dashed red. The placement module only reads the data model and conics.fit_tracks, and the solver files are not edited.

One code path, three callers. calibrate-lh2 reframe --shift --rotate of a spin calibration (which has no placement samples to re-solve from, so reframe failed on one) now goes through place_calibration, turning about the frame's zero. site init --from-calibration places its calibration through it too, and a test pins that it writes the same calibration id as before.

The editor gains a Calibration tool (C). The operator types an id prefix, tag or path (a list of every calibration found is offered, and the newest is never picked for them), and the calibration is drawn over the site in its own frame. Dragging it moves it with the zero on the snap grid, a handle turns it in 90 degree steps about its centre (Alt for free), and there is no scale handle. Shift and turn are also typed numbers. Save calibration writes a new calibration under ~/.dotbot/calibrations/<site>/ (the file it was loaded from is never written), names the push command (dotbot swarm calibrate-lh2 push <id> --site <name>, with --site-changed when it came from another site), and says that robots holding the old placement draw in the wrong place until pushed. Its tag is the source's suffixed with the site, numbered if that tag is taken. A corner-collected calibration is drawn with a refusal, and Save stays disabled until Re-anchor is ticked. Save is also blocked while the site's anchor or extent have unsaved changes, since the placement reads them from the file.

Routes: GET /api/calibrations (each file's id, created time, kind and stations), GET /api/calibrations/{id} (the overlay: fence, each station's index, channel, rectangle and centres, links), POST /api/calibrations/{id}/place with {dx_mm, dy_mm, theta_deg, reanchor} (200 with the new id, path and push command, 422 with the refusal).

Plan: rendered copy https://claude.ai/artifact/LHUXjhx1nMWifC2oGRBayt, source https://github.com/DotBots/dotbot-workspace/blob/main/plans/site-editor/plan.html (both internal).

Never overwriting. A placement's id comes from its stations, not its tag or anchor, so the same move saved twice lands on the same id: the file already there is kept as it is and returned, rather than rewritten with a renumbered tag. A move that leaves the id of the source (for example reframe --shift 0,0 into the source's own site) is refused, so the source can never be written. reframe of a spin calibration and site init save through the same helper and number taken tags the same way as the editor. After Save the editor keeps the source drawn where it was moved to and lists the new file at once.

Validation. pytest: a spin fixture placed with a 90 degree turn and an offset lands every spin centre where the move puts it within 0.1 mm, keeps distances (scale), writes a new id in the site with the original object untouched; a two-station fixture with per-station rectangles moves as one body on a site without an extent, rectangles boxed ((0, 0, 1500, 1500) turned and shifted to (2000, 100, 3500, 1600)), fence the union; corner refusal and re-anchor; below-zero refusal naming the station and channel; tag numbering; reframe of a spin calibration writes the id place_calibration gives; site init's placement is the editor's. Server tests: listing, overlay by id, 404, a placement leaving the source file byte-identical, 422s, the extent warning. vitest: the rigid move and its turn about a pivot, the snapped angle, loading and saving a placement with the body sent, the corner refusal, a drag landing on the grid. npm run lint, typecheck and build; pre-commit run --all-files; full pytest 1799 passed, vitest 850 passed (after the move onto the LH2 stack: pytest 1888 passed plus one known, unrelated camera-service test failure, vitest 864 passed, typecheck clean); also the fence refused below zero after a turn, a re-anchored calibration without tracks held to zero, the same placement saved twice keeping the first file.

By hand, headless, in a throwaway home: dotbot site new a 5 x 6 m site, a one- and a two-station spin fixture plus a corner fixture under ~/.dotbot/calibrations/; over REST the corner one is refused (422), a 180 degree turn is refused below zero, and a 90 degree placement writes a new file while the source's sha256 is unchanged; in headless Chrome the overlay is loaded, dragged, turned with the handle (snapped to 90 degrees, zero on the 50 mm grid) and saved, and the panel shows the push command. reframe --shift of a spin calibration writes the placed file. That pushed robots then draw in the right place can only be proven on the bench.

Docs (doc/cli/, the LH2 guide) follow after review.

+ - Files
#315 and the LH2 stack to #343, carried underneath (merge separately) +7495 -383 78
This PR, tests +535 -0 4
This PR, source +1125 -19 12
This PR's files + -
dotbot/calibration/placement.py +348 -0
dotbot/tests/test_calibration_placement.py +257 -0
dotbot/console-web/src/siteEditor/Canvas.tsx +175 -7
dotbot/console-web/src/siteEditor/CalibrationPanel.tsx +179 -0
dotbot/site_placement.py +158 -0
dotbot/tests/test_site_placement.py +142 -0
dotbot/console-web/src/siteEditor/siteEditor.test.tsx +100 -0
dotbot/console-web/src/siteEditor/SiteEditor.tsx +79 -4
dotbot/console-web/src/siteEditor/rigid.ts +57 -0
dotbot/console-web/src/siteEditor/types.ts +52 -0
dotbot/console-web/src/siteEditor/api.ts +36 -1
dotbot/console-web/src/siteEditor/rigid.test.ts +36 -0
4 small files: swarm_lh2.py, site_cmd.py, Inspector.tsx, dotbot/site_editor.py +41 -7
Total, 16 files +1660 -19

@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.38901% with 199 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.34%. Comparing base (ddc43f7) to head (40c933b).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
dotbot/console-web/src/siteEditor/SiteEditor.tsx 83.06% 74 Missing ⚠️
dotbot/console-web/src/siteEditor/Canvas.tsx 83.61% 69 Missing ⚠️
dotbot/console-web/src/siteEditor/api.ts 80.26% 15 Missing ⚠️
dotbot/console-web/src/siteEditor/main.tsx 0.00% 8 Missing and 1 partial ⚠️
dotbot/console-web/src/siteEditor/Inspector.tsx 96.90% 7 Missing ⚠️
dotbot/cli/swarm_lh2.py 91.80% 5 Missing ⚠️
dotbot/console-web/src/siteEditor/edit.ts 96.59% 5 Missing ⚠️
dotbot/tests/lh2_multi_fixture.py 94.80% 4 Missing ⚠️
dotbot/console-web/dev/fake_swarmit_server.py 0.00% 3 Missing ⚠️
...ot/console-web/src/siteEditor/CalibrationPanel.tsx 97.76% 3 Missing ⚠️
... and 4 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #326      +/-   ##
==========================================
+ Coverage   88.21%   88.34%   +0.13%     
==========================================
  Files         227      238      +11     
  Lines       36147    38346    +2199     
  Branches     2324     2658     +334     
==========================================
+ Hits        31886    33877    +1991     
- Misses       4257     4464     +207     
- Partials        4        5       +1     
Flag Coverage Δ
console 82.81% <88.58%> (+0.81%) ⬆️

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

Files with missing lines Coverage Δ
dotbot/cli/main.py 95.83% <ø> (ø)
dotbot/cli/site_cmd.py 91.54% <ø> (-0.04%) ⬇️
dotbot/config.py 98.59% <ø> (-0.29%) ⬇️
dotbot/console-web/src/BotMarker.tsx 100.00% <100.00%> (ø)
dotbot/console-web/src/GridView.tsx 88.20% <100.00%> (-0.22%) ⬇️
dotbot/console-web/src/ListView.tsx 88.78% <100.00%> (+0.64%) ⬆️
dotbot/console-web/src/LocalizationPanel.tsx 86.09% <100.00%> (ø)
dotbot/console-web/src/MapView.tsx 97.54% <100.00%> (+<0.01%) ⬆️
dotbot/console-web/src/botFacts.ts 98.10% <100.00%> (+0.66%) ⬆️
dotbot/console-web/src/heldWaypoints.ts 100.00% <100.00%> (ø)
... and 42 more

... and 3 files 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.

AI-assisted: Claude Opus 5.5
AI-assisted: Claude Opus 5.5
… page

Any web page in the browser can send requests to 127.0.0.1 with a valid
Host header, and behind a permissive CORS policy read the replies, so the
Host check alone let it save site.toml; a server bound beyond loopback
took any Host. The peer address, Origin and Sec-Fetch-Site are checked.

AI-assisted: Claude Opus 5.5
…ng tables

A patched file is read back and must give the model that was sent, so a
layout the patcher does not follow is refused rather than written.

AI-assisted: Claude Opus 5.5
Breaking: schema 3 calibration files are refused, so a calibration is
collected again. swarmit's own calibration reader still expects schema 3
and needs its matching change before the dashboard reads these files.

AI-assisted: Claude Opus 5.5
…tangle

Breaking, cross-repo: the messages and the config page (magic SWRP, 872
bytes) only match a swarmit bootloader and net core that store stations
by mask, and swarmit's Python must accept mask messages before it relays
them. Robots below device info 4 are refused with a reflash.

AI-assisted: Claude Opus 5.5
…angles

A station that never sees part of the site yields no sweep pair there,
so the extent drops nothing a station could solve, whereas the box around
its spins dropped areas such as a staging strip the robots never spun in.

AI-assisted: Claude Opus 5.5
A robot with no heading first gets a 40 mm probe move toward its first
point, and its batch once it has a heading and the probe is done. While
waypoints_awaiting_heading is set, the waypoint report and the mode are
the probe's, so a client watching mode alone sees an early arrival.

AI-assisted: Claude Opus 5.5
… a file

A placement's id comes from its stations, not its tag or anchor, so the
same move saved twice, or a move into the source's own site, landed on an
existing path; save_placed keeps that file and refuses the source's id.

AI-assisted: Claude Opus 5.5

This branch has not been deployed

No deployments
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