Sites, maps & zones¶
Everything that is only meaningful relative to one mapped place — the Nav2 map
pair, the slam_toolbox posegraph, and named zones — lives together as a
site bundle. A zone's pose is a coordinate in a map frame whose origin is
an accident of where SLAM happened to start, so those three artefacts must live
and travel as one unit or they quietly stop describing the same building.
- A floor is one SLAM session, i.e. one map frame.
- A site groups floors that share a location.
- A revision is one immutable set of map artefacts for a floor.
The layout is managed by mote_bringup/sites.py (pixi run site); what a
revision must contain is mote_bringup/bundle.py, which is ROS-free and
shared with the fleet server so both ends validate a revision with the same
code.
The layout¶
~/.mote/active.yaml -> {site: home, floor: ground}
~/.mote/sites/<site>/
site.yaml -> {schema: 1, name, default_floor}
floors/<floor>/
zones.yaml named poses in this floor's map frame
map -> maps/<rev>/ symlink to the current revision
maps/<rev>/ immutable once published:
map.yaml + map.png the nav2 map_server pair — the cleaned
map, the one served and distributed
map_raw.png the untouched map_saver output, kept for
provenance (same frame as map.png)
diagnostics.png before/after + detected structure
map.posegraph + .data the slam_toolbox graph, so mapping can
continue later in the same frame
meta.yaml provenance: when, which mapping bag, and
the cleaning pass parameters and stats
Site bundles are per-robot state, so they live under MOTE_HOME (~/.mote by
default) with the rest of it — never in the package, which is shared config an
update may replace. Setting MOTE_HOME moves the lot, which is how tests and
the sim get their own.
The whole bundle is plain files and YAML, so it can be zipped, synced, or served by a web API without translation. That is exactly what the map registry does.
Revisions, and why the symlink¶
A revision is staged completely inside maps/<rev>/ before the map symlink
is flipped to it — one atomic rename. A half-written save, a crash, or an
interrupted transfer is therefore never visible to a reader, and a rollback is
just flipping to an older revision:
pixi run site list # every site and floor
pixi run site info # the active floor: revisions, zones, footprints
pixi run site use-map <rev> # roll back
The newest revisions are kept and older ones pruned.
Saving a map¶
save-map stores the posegraph alongside the map, which is what lets a later
session continue in the same frame. Extend a map; do not remap it — remapping
starts a new frame and every zone taught in the old one becomes wrong.
Mapping runs record a rosbag by default (mapping_launch.py record:=true; the
sim passes false) and save-map stamps that session's bag into the revision's
meta.yaml, so site info can say which drive produced which map.
save-map also runs the same validation the fleet server runs on an upload, so
a map the server would refuse is refused while the mapping session is still up
and can be extended.
The cleaning pass¶
save-map automatically runs an FFT structure-extraction pass over the map. It
keeps the untouched map_saver output as map_raw.png and promotes the
decluttered image to the served map.png, alongside a diagnostics.png. So
navigation always consumes the cleaned map while the raw one is retained for
provenance. Both share a frame, so localisation and zones are unaffected, and a
cleaning failure falls back to serving the raw map. The posegraph belongs to
the raw map — mapping continuation extends from raw, never from the cleaned
image. Details: map cleaning.
Zones¶
A zone is the one named-place concept: a taught pose in the floor's map
frame that the robot can navigate to. goto <zone> drives to any of them, and
fetch uses them as its pickup and drop waypoints. A zone may optionally
carry an area footprint — a circle or a polygon — which turns a bare
waypoint into something that also answers "am I inside it?". That is optional
metadata on one concept, not a second kind of thing: one YAML section, one
loader, one teach command.
Teach a zone by driving there, not by editing YAML:
Re-teaching a zone's pose keeps whatever footprint it already had; passing
--radius is the deliberate way to replace one.
Rooms do not have to be taught one at a time, either:
pixi run segment-map # propose one polygon zone per room
pixi run segment-map --write # merge the proposal into zones.yaml to rename
segment-map carves a saved map's free space into rooms on one physical
assumption — a doorway is narrow. It is additive over hand-taught zones (a
candidate covering an already-footprinted zone is dropped), so re-running is a
no-op, and it writes beside zones.yaml, never into the immutable map
revision. Two consequences worth knowing: a corridor network is not proposed at
all, and the geometry is Manhattan after rotation. See
map cleaning & room segmentation and the
validation run.
The shape of the file, circles versus polygons, and how membership is answered are covered in Missions.
Names travel, coordinates do not¶
Beside its coordinates, a zone carries a vocabulary: kind,
display_name, aliases, navigable, parent, tags. Every field is
optional — an untouched zone is a navigable area — and save-zone --kind
teaches the one field it can know.
That split is the whole reason the vocabulary exists separately. (2.0, 3.5)
is a different physical point for the robot standing beside this one, and no
fleet-level transform fixes that; the name is true for both. So the fleet
publishes the vocabulary and not the binding, over
GET /v1/zones — expressed by the route rather than
by a rule someone has to remember: everything under /v1/maps is bound to a
basemap, everything under /v1/zones is bound to nothing.
Locally, load_zones refuses a vocabulary in which two zones answer one
query, because loading it would resolve goto by dictionary order — silently,
once per boot, and differently after an edit.
Publishing a map to the fleet¶
Saving and publishing are deliberately separate: saving has to work on a robot that has never seen a fleet server.
An upload changes nothing. The revision sits inert until an operator promotes
it, at which point the floor's canonical map flips and a retained MQTT
message hands it to every agent — including one that was switched off for the
whole mapping session. That inertness is also the conflict answer: two robots
that map one floor leave two candidates, never a merge.
A pulled revision replaces the floor's zones.yaml (keeping the old one as
zones.<old-rev>.yaml), because a different session's map makes previously
taught zones wrong, and takes effect on the next bringup, since
map_server reads its map at startup.
The operator flow is in the fleet runbook; the routes are in the fleet API contract.