Skip to content

Fleet API: serve the zone vocabulary from the site bundle - #87

Merged
MJohnson459 merged 1 commit into
mainfrom
fleet-zone-vocabulary
Jul 31, 2026
Merged

Fleet API: serve the zone vocabulary from the site bundle#87
MJohnson459 merged 1 commit into
mainfrom
fleet-zone-vocabulary

Conversation

@MJohnson459

Copy link
Copy Markdown
Contributor

Fleet API: serve the zone vocabulary from the site bundle

Branch fleet-zone-vocabulary (commit b5e8fcb), committed not pushed.

What changed

GET /v1/zones and GET /v1/zones/<site>/<floor> serve the zone vocabulary
from the same site bundles the server already reads for maps, shaped as
zone/v0: names, kinds, display names, aliases, navigable, parent, tags — and
no coordinates.

The split is expressed by the route, not by a rule someone has to
remember: everything under /v1/maps is bound to a basemap and served to a
client that already has one; everything under /v1/zones is bound to
nothing. /v1/maps/<site>/<floor>/zones.json keeps its shape and its
coordinates, so the dashboard is unaffected — a dispatcher that must never
be handed a map gets /v1/zones and only that.

Supporting work, since a vocabulary needs something to carry:

  • zones.yaml gains optional vocabulary fields. All optional, so no existing
    file needed rewriting — an untouched zone is a navigable area.
  • The rules live once in the ROS-free mote_bringup/bundle.py (zone_term,
    ambiguities, check_vocabulary, vocabulary), shared by the robot,
    save-map and the fleet server, as M4 established.
  • save-zone --kind teaches the one field it can know; segment-map emits
    kind: room because what it segments are rooms; re-teaching a pose
    carries the vocabulary through and bumps vocabulary_revision.
  • goto/fetch resolve aliases and display names, and refuse a
    non-navigable zone rather than driving to it — fetch explicitly, because
    falling through to its label branch would send the detector hunting for an
    object called "keepout".
  • Docs: fleet-api.md (new contract section), README.md §12 (operator
    flow), CLAUDE.md (new "Fleet: the zone vocabulary" section).

Four decisions worth reviewing

  1. Built, not stripped. The payload is constructed from the fields a
    vocabulary may carry, never filtered of the ones it may not. Stripping
    holds only until someone adds a geometry key to zones.yaml and never
    reads this code, and the leak would be a plausible-looking coordinate
    rather than a crash. The test walks the whole payload for geometry-shaped
    keys at any depth rather than checking the ones it thought of.

  2. The vocabulary is not gated on a published map, where the binding
    rightly is. Names are a fact about the building, so a floor someone has
    named but no robot has mapped still answers. That is the portability the
    split buys, and gating it would have quietly given it back.

  3. problems is reported, not enforced. An ambiguous alias, or a name a
    dispatcher cannot type (Café), leaves the map perfectly good — refusing
    to serve a floor's basemap over one would be the wrong price. The name is
    served verbatim rather than slugified to cafe, because inventing one is
    a rename nobody asked for. The robot does refuse an ambiguous vocabulary
    at load_zones, since it could not honour goto unambiguously.

  4. Contradictions are refused at the parse — an unknown kind, a
    keepout marked navigable: true, aliases that are not a list. Not
    ambiguities to report; honouring the last one written would make the flag
    mean whatever was typed most recently.

Verification

  • 304 tests pass. The 6 skips are the pre-existing broker-dependent e2e
    tests (no MQTT path is touched). Confirmed the 8 failures I first saw when
    running all of mote_bringup/test in one pytest process reproduce
    identically on the unmodified base commit — shared rclpy context, not this
    change.
  • New mote_fleet/test/test_zone_vocabulary.py (17 tests) over a real
    socket; new vocabulary tests in test_zones.py, test_goto_command.py,
    test_parse_command.py, test_segmented_zones.py.
  • pre-commit clean across the tree.
  • Drove the real server over a socket: vocabulary for two floors (one never
    mapped), the binding for contrast, and the binding 404ing on the unmapped
    floor.
  • gen_hospital.py regenerated; SDF output byte-identical, only zones.yaml
    changed.

Not pushed, per the standing rule that remote git operations need your
approval — voro pr 260 when you are ready.

Related: mote #249 (rest of spec v0), AugereAI #192/#193.

The API served the roster, the basemaps and dispatch, but not the one thing
a dispatcher most needs -- what places can I name? Zone names lived only in
the site bundle on the robot, so a caller had to configure them out of band
or scrape the list a robot prints when it refuses an unknown zone, which is
an accident of an error message rather than a contract.

GET /v1/zones and /v1/zones/<site>/<floor> answer it from the same site
bundles the server already reads for maps, shaped as zone/v0.

Names travel; coordinates do not. A zone's pose is a coordinate in one
robot's map frame, whose origin is an accident of where its SLAM session
started, so it is a different physical point on the robot beside it and no
fleet-level transform fixes that. The name is true for both. So the
vocabulary (name, kind, display_name, aliases, navigable, parent, tags) is
published and the binding is not, and the split is expressed by the route
rather than by a rule someone has to remember: everything under /v1/maps is
bound to a basemap and served to a client that already has one, everything
under /v1/zones is bound to nothing. zones.json keeps its shape and its
coordinates; the dashboard is unaffected.

Four things are load-bearing:

- The payload is *built* from the fields a vocabulary may carry, never
  stripped of the ones it may not. Stripping holds only until someone adds a
  geometry key to zones.yaml and never reads this code, and the leak would
  be a plausible-looking coordinate rather than a crash.
- The vocabulary is not gated on a published map where the binding rightly
  is. Names are a fact about the building, so a floor someone has named but
  no robot has mapped still answers -- that is the portability the split
  buys, and gating it would have quietly given it back.
- problems is reported, not enforced. An ambiguous alias or a name a
  dispatcher cannot type (Café) leaves the map perfectly good, and refusing
  to serve a floor's basemap over one would be the wrong price. The name is
  served verbatim rather than slugified, because inventing one is a rename
  nobody asked for. The robot does refuse an ambiguous vocabulary at
  load_zones, since it could not honour goto unambiguously.
- Contradictions with no reading at all -- an unknown kind, a keepout marked
  navigable: true -- are refused at the parse by the shared ROS-free
  bundle.py, so save-map catches them locally and the server on upload.

Every vocabulary field is optional, so no zones.yaml needed rewriting: an
untouched zone is a navigable area. save-zone --kind teaches the one field
it can know, segment-map emits kind: room because what it segments are
rooms, and re-teaching a pose carries the vocabulary through and bumps
vocabulary_revision -- a better coordinate is not a rename.

goto and fetch now resolve aliases and display names, and refuse a
non-navigable zone rather than driving to it -- fetch explicitly, because
falling through to its label branch would send the detector hunting for an
object called "keepout".

Verified: 304 tests pass (the 6 skips are the pre-existing broker-dependent
e2e tests; no MQTT path is touched), pre-commit clean, and the new routes
driven over a real socket -- the vocabulary walked for geometry-shaped keys
at any depth, the binding asserted to still carry them, and a named-but-
unmapped floor serving a vocabulary while its binding 404s.

Related: mote #249 (the rest of spec v0), AugereAI #192/#193.
@MJohnson459
MJohnson459 merged commit d265932 into main Jul 31, 2026
4 checks passed
@MJohnson459
MJohnson459 deleted the fleet-zone-vocabulary branch July 31, 2026 21:37
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