Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ Maps live in **site bundles** under `~/.mote/sites/`.
SLAM, Nav2, and teleop.**

The longer version of all of the above — provisioning a clean Pi, what a site
bundle holds, the systemd services, teaching zones and running missions — is
bundle holds, the systemd services, naming zones and running missions — is
[Getting started](https://clachdev.github.io/Mote/getting-started/) on the docs
site.

Expand Down
4 changes: 2 additions & 2 deletions docs/design/fleet.md
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ duplicated in two places — one small refactor to factor out at M4.)
**Conflict: two robots map the same floor.** Do **not** auto-merge. A map frame's
origin is *"an accident of where SLAM started, so zones/map/posegraph must live and
travel together"* (`sites.py` docstring); silently merging two frames breaks every
taught zone coordinate. Instead the server keeps both as **candidate revisions**,
bound zone coordinate. Instead the server keeps both as **candidate revisions**,
and an operator **promotes** one to canonical — the same `site use-map` semantics,
centralised. Nothing is lost, nothing is silently merged, and the loser is retained
for audit (matching the raw-map-retention ethos already in `save-map`).
Expand Down Expand Up @@ -1013,7 +1013,7 @@ M7 (security hardening) : cross-cutting, folds into each; can start after M0
must not leave a floor half-promoted — the server re-announces every floor at
startup, which repairs it; and zones travel *inside* the revision and replace
the floor's on install, because a different session's map makes previously
taught zones wrong. The one thing the milestone does not do is make a pulled
bound zones wrong. The one thing the milestone does not do is make a pulled
map take effect live: `map_server` reads its map at startup, so a flip lands
on the next bringup, and health now carries the revision each robot is
actually running so the gap is visible.
Expand Down
27 changes: 15 additions & 12 deletions docs/design/mapping-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ road at nearly every stage.
whose design assumes the robot built the map. Distribution order ran
backwards (robot first, registry second).
- Zones: `segment-map` proposed seven placeholder rooms; renaming is manual;
the one taught pose (`office`) was invalidated by the new frame and must be
re-taught by driving to it. The candidate was published with placeholder
the one taught pose (`office`) was invalidated by the new frame and had to
be captured again (the dashboard's zone editor, which can now place it on the
candidate, did not yet exist). The candidate was published with placeholder
names because packing happens at publish time.
- The bags themselves — the actual source — live in the robot's
`~/.mote/bags/` under a pruner that trims older bags on every recording run.
Expand Down Expand Up @@ -188,13 +189,14 @@ Requirements, in priority order:
as canonical zones draw today (circle, polygon, waypoint cross), plus the
carry-forward report: which names re-bound automatically, which are proposed
matches, which rooms are new placeholders.
- **Edit before promoting.** Rename a zone, accept/reject a proposed match,
edit aliases/kind, adjust or delete a polygon, and **click-to-teach a pose**
(place `office` by clicking the office on the map — replaces drive-to-teach
for goto targets; taught-by-driving remains for poses that need real
approach headings). Edits write back to the candidate's `zones.yaml` on the
server (bumping `vocabulary_revision`); the candidate stays inert
throughout.
- **Edit before promoting.** *Built* (task 339's review pane,
`server/ui/zone_editor.mjs`): rename a zone, write its note, adjust or delete
a polygon, and place a pose by clicking the map (`⌖`), which is what replaced
driving to a goto target — `save-zone` remains for poses that need a real
approach heading. A save derives a new candidate from the one under review
rather than writing into it (bumping `vocabulary_revision`); the source's
bytes never change and the result stays inert until promoted. Still to do:
accepting or rejecting a proposed carry-forward match.
- **See the build report.** The scoring diff from stage 2, on the same page
as the promote button.
- **Promote.** Unchanged M4 semantics: audited operator action, symlink flip,
Expand Down Expand Up @@ -285,9 +287,10 @@ Sized so each is one dispatchable task; existing tasks noted.
report.
7. **Candidate preview** — task 339 (see the map you promote), extended with
the zones overlay.
8. **Candidate zone editing** — rename/alias/kind/polygon/click-to-teach on
the candidate from the dashboard; server-side writes to candidate
`zones.yaml`, audited, inert until promotion.
8. **Candidate zone editing** — *done* (task 339): rename, note, polygon and
click-to-place on the candidate from the dashboard, deriving a new
candidate, audited, inert until promotion. What remains is accepting or
rejecting a carry-forward match, which needs step 6.
9. **Build report on the review page** — stage-2 scoring diff rendered beside
the promote picker.
10. **An orientation estimator the alignment step can be gated on** — a
Expand Down
35 changes: 22 additions & 13 deletions docs/fleet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ M4, robots publish into rather than an operator rsyncing (§11). A robot with no
basemap on the server still appears in the roster with its health and its mission;
the map pane says so rather than drawing an empty grid.

**Taught zones are drawn on the basemap**: a circle for a `radius` footprint, an
**A floor's bound zones are drawn on the basemap**: a circle for a `radius` footprint, an
outline for a `polygon`, a cross for a bare waypoint, each labelled — so the
`goto <zone>` targets you can type are the ones you can see. They come from the
canonical revision, in that revision's map frame.
Expand Down Expand Up @@ -730,7 +730,7 @@ and the detail pane. Two things follow from losing the side-by-side view:
**The dispatch form is generated from the robot's own capability set.** It
arrives retained on the broker, so the page knows the keys and the input shapes
without asking; a select lists what this robot offers, and one field appears per
input property. A field becomes a **zone picker** — the taught zones of the
input property. A field becomes a **zone picker** — the bound zones of the
floor on screen — exactly when its schema `$ref`s zone/v0's zone reference,
which is what that `$ref` is for. So the page contains no list of capabilities
and no list of which inputs are places, a robot that grows a capability grows
Expand Down Expand Up @@ -942,7 +942,7 @@ which appears whenever the floor on screen has something waiting. It shows:
can be continued in this frame), its bytes and digest.
- **The zones in it**, and — the part that is easy to miss — an `inherited`
mark beside the heading when they are not the revision's own. A revision that
carries no zones is drawn with the floor's, taught in a previous session's
carries no zones is drawn with the floor's, bound in a previous session's
frame: they draw perfectly over the new map and are out by however far the two
origins differ, which the canvas cannot show. Zones that belong to the map
they are drawn on are marked nothing at all — that is what "zones" means.
Expand Down Expand Up @@ -986,9 +986,18 @@ The controls are the map and the list together:
claims precision the map does not have. A whole zone moves by whole pixels, so
a traced room keeps its shape. **Hold shift to move freely**, for the rare
case that wants a coordinate between two pixels. Only what you drag is
snapped: a pose taught by driving a robot there is a measurement, and it is
left exactly where the robot said, while an outline this editor invents starts
on the grid.
snapped, whoever put it there — a pose a robot measured by driving to it and
one an earlier edit placed are alike in being already recorded — while an
outline this editor invents starts on the grid. Drag a pose and it does snap:
a drag is a fresh coordinate, with the map's precision and no more.
- **What you move stops claiming a robot drove there.** Each zone's coordinate
records how it came to be — `taught` when a robot was driven there and
`save-zone` captured the pose, `derived` when `segment-map` read it off the
map — and that is what tells whoever looks next whether a re-map invalidates
it. Anything you place or drag here is neither, so it is saved as `external`,
stamped with your operator name and the server's clock. A zone you did not
touch keeps what it had, so a segmented room stays `derived` until somebody
reshapes it.
- **It is the same list either way.** The zones of a revision are listed under
the map whether or not you are editing them — the name, then whether it is a
**point** or an **area** — and `edit zones` puts controls into those rows
Expand Down Expand Up @@ -1045,7 +1054,7 @@ it looks right. Two consequences worth knowing:
revision plus the five newest candidates, so the intermediates fall off on
their own.
- **A revision that inherited the floor's zones stops inheriting.** The saved
candidate carries them, which is what you want: inherited zones were taught in
candidate carries them, which is what you want: inherited zones were bound in
another session's frame, and dragging them onto this map is the correction.

`cancel` discards the edit. There is no autosave and nothing is written until
Expand All @@ -1068,7 +1077,7 @@ digest, stages it in a temporary directory, renames it into `maps/<rev>/` and
flips its local `map` symlink. A half-transferred revision is never visible.

**Zones travel with the map.** A revision from a different mapping session is a
different map frame, so the zones taught in the old one are wrong the moment the
different map frame, so the zones bound in the old one are wrong the moment the
new map is published — the bundle's `binding.yaml` therefore replaces the floor's,
and the one it replaces is kept beside it as `zones.<old-rev>.yaml`.

Expand All @@ -1083,7 +1092,7 @@ how the dashboard shows a robot that has not picked the new map up yet.
Nothing is merged, and nothing is lost. Both are candidates, an operator
promotes one, and the other is retained. This is not a limitation to fix: a map
frame's origin is an accident of where SLAM started, so silently merging two
frames would break every taught zone coordinate. If both robots proposed the
frames would break every bound zone coordinate. If both robots proposed the
same revision id (they are per-second timestamps), the second is stored as
`<rev>-2` and `fleetctl sites <site> <floor>` shows which robot uploaded which.

Expand Down Expand Up @@ -1152,9 +1161,9 @@ pixi run save-zone sluice --radius 0.8 --no-navigable
```

`--no-navigable` marks a place a robot must not be sent to — `goto sluice` is
then refused by the robot rather than driven to. Re-teaching a pose (`save-zone
"the kitchen"` again) keeps the note and the flag: a better coordinate is not a
rename.
then refused by the robot rather than driven to. Capturing a pose again
(`save-zone "the kitchen"`) keeps the note and the flag: a better coordinate is
not a rename.

Other names a place answers to belong in the note. There is no alias list: the
mission layer's resolver reads free text and already knows what a store room is,
Expand All @@ -1170,7 +1179,7 @@ zones:
note: the good kettle is in the store room}
```

A floor taught before place-names still loads without being re-taught: its
A floor written before place-names still loads unchanged: its
`kind`, `display_name`, `aliases`, `parent` and `tags` are accepted and dropped,
its `description` is read as the note it was, and `kind: keepout` still means
`navigable: false`. What it loses is alias matching — `goto galley` no longer
Expand Down
24 changes: 17 additions & 7 deletions docs/fleet/fleet-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ overwriting the directory.

### `GET /v1/maps/<site>/<floor>/zones.json`

The floor's taught places **with their coordinates**, in the same map frame as
The floor's bound places **with their coordinates**, in the same map frame as
the basemap, so the dashboard can draw them and an operator can see the `goto`
targets they are about to type. This is the zone **binding**: it is served
beside the basemap, to a client that already has the basemap, and it is not
Expand All @@ -266,9 +266,9 @@ below.
```

Read from the **canonical revision's** `zones.yaml`, falling back to the
floor-level file for a bundle seeded by rsync. `404` for a floor with no taught
zones — an empty list would claim the floor has none, which is a different
statement — and `404` for a floor with no published map, because a coordinate
floor-level file for a bundle seeded by rsync. `404` for a floor whose
revision binds nothing — an empty list would claim the floor names no places,
which is a different statement — and `404` for a floor with no published map, because a coordinate
with no map frame to be in is not an answer.

---
Expand Down Expand Up @@ -323,7 +323,7 @@ where the stationery lives.
| `name` | What the place is called, which is also what a dispatcher types. Printable text with no leading or trailing space; unique within a **floor**, not within a site — two floors may each have a `reception`. Matched exactly, then case-insensitively and whitespace-normalised. |
| `note` | Free text for where reality diverges from what the name implies. The other names a place answers to belong here: a resolver reads the sentence, and there is no alias list to keep in step by hand. |
| `navigable` | Whether it is a legal destination. Not vocabulary — it is the planner's contract — but it travels with the names because it is not a coordinate. |
| `revision` | Bumped every time a zone is taught, so a binding can record which vocabulary it was built against. |
| `revision` | Bumped every time a zone's vocabulary is written, so a binding can record which vocabulary it was built against. |
| `problems` | Empty when the vocabulary is well-formed; see below. |

`kind`, `display_name`, `aliases`, `parent` and `tags` were part of this
Expand Down Expand Up @@ -390,7 +390,7 @@ The shape of it is one rule:

That is also the conflict answer. Two robots that map the same floor produce two
candidates, both kept, neither merged — a map frame's origin is an accident of
where SLAM started, so merging two frames would break every taught zone
where SLAM started, so merging two frames would break every bound zone
coordinate (fleet.md Q4). The loser is retained for audit.

**A revision is an immutable directory, and distribution is a copy plus one
Expand Down Expand Up @@ -529,7 +529,7 @@ which looks entirely convincing and is the exact failure this route removes.

`source` is `revision` when the revision carries its own `zones.yaml` and
`floor` when it inherits the floor's. The difference matters and the coordinates
cannot express it: inherited zones were taught in a *previous* SLAM session's
cannot express it: inherited zones were bound in a *previous* SLAM session's
frame, so they draw perfectly over this map and are wrong by however far the two
origins differ.

Expand Down Expand Up @@ -591,6 +591,16 @@ frame they were drawn in: the operator is looking at that revision's own map.
Omitted, the canonical revision is edited, which is the same thing for a floor
whose published map is what is on screen.

**An entry's `anchor` is carried, not re-invented.** zone/v0's
`anchor.method` says how a coordinate came to be — `taught` for a pose a robot
was driven to, `derived` for one an algorithm read off a map — and a submitted
entry keeps whatever it names, so a zone this edit did not touch keeps its
provenance. The dashboard's editor sends `{"method": "external", "by":
"zone-editor"}` on geometry it placed or moved, because neither of the other
two is true of a click; the server fills in `at` from its own clock and
rewrites `by` to name the operator holding the token, which is the half a
browser cannot be trusted for. A method outside zone/v0's four is a `422`.

**The bar is the source's, not the upload's.** A revision with no posegraph is
one mapping cannot be continued from — an error for a robot's upload, where the
session can be re-run, and a *warning* on a stored revision, which navigates
Expand Down
6 changes: 3 additions & 3 deletions docs/fleet/m4-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,11 @@ scratch registry seeded from the committed sim bundles, and one robot's retained
presence/health/pose published straight to the wire — driven with a headless
Chromium.

![The fleet map with taught zones and the revision picker](../images/fleet-map-registry.webp)
![The fleet map with the floor's zones and the revision picker](../images/fleet-map-registry.webp)

What the picture verifies, none of which a unit test can:

- **Taught zones land on the map.** The four dashed rectangles are
- **The floor's zones land on the map.** The four dashed rectangles are
`hospital_world`'s ward polygons drawn through the Q5 transform, and they sit
exactly on the room walls in the basemap. `pickup`, `home` and `dropoff` are
bare waypoints and draw as labelled crosses.
Expand Down Expand Up @@ -226,7 +226,7 @@ a reason:
5. **Zones travel inside the revision**, and replace the floor's on install (the
old file is kept as `zones.<old-rev>.yaml`). The design says map and zones
must travel together; the consequence nobody writes down is that installing a
*different* session's map makes the previously taught zones wrong, so leaving
*different* session's map makes the previously bound zones wrong, so leaving
them in place would be the silent failure the rule exists to prevent.

Also worth recording: `save-map` now runs the same validation locally, so a map
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ autonomously — run it *on the Pi*, so a WiFi drop cannot end the mission.
`save-map` writes an immutable map revision into the active site's floor, runs a
cleaning pass over it, and validates it. [Sites, maps &
zones](robot/sites.md) explains what a site bundle is, why maps are revisions,
and how to teach named places.
and the three ways a named place gets a coordinate.

## 6. Drive the map

Expand Down
Loading