Skip to content

A promoted map must load on a running robot without a nav restart - #120

Merged
MJohnson459 merged 1 commit into
mainfrom
map-hot-reload
Sep 29, 2026
Merged

MJohnson459 merged 1 commit into
mainfrom
map-hot-reload

Conversation

@MJohnson459

Copy link
Copy Markdown
Contributor

A promoted map loads on a running robot without a nav restart

map_server reads its map once, at startup. On mote-01 (2026-09-21) a promoted revision was pulled and installed, and Nav2 kept serving the old one until pixi run robot was restarted by hand. Health read the floor's symlink, so the fleet showed the new revision throughout.

Where the reload lives

In a new node, map_reloader (mote_bringup/map_reloader.py; the ROS-free half is in map_reload.py). nav2_launch.py starts it only with localisation:=true, because under SLAM there is no saved map to swap. The agent stays a bridge and a reporter: it publishes a latched map/installed (a statement of what it put on disk) and never calls Nav2.

  • Idle robot: the node calls map_server/load_map with maps/<rev>/map.yaml, not the symlink, so the served file is the one reported. It then re-publishes the last amcl_pose on /initialpose and publishes a latched map/serving. AMCL re-initialises its filter on a new map; the sim log shows Received a 117 X 117 map followed by the carried pose. The revision shares the floor frame, so the old estimate is still correct. The node reads amcl_pose rather than TF so it holds no TransformListener.
  • Mission in flight: the node asks the task server's new task/idle service (std_srvs/Trigger) and waits until the mission reaches a terminal state. While a load runs, map/serving.loading makes the task server refuse new missions as busy (recoverable). The refusal is bounded at 30 s, so a reloader that dies mid-load cannot hold the lane.
  • Nav not running: nothing changes; the next bringup reads the symlink.
  • Zones: the task server reloads the floor's zones.yaml when the served revision changes, so a promoted zone edit also needs no restart.
  • Failed load: the old map stays served, the reason is logged and reported in map/serving.error and health's map.error, and the node does not retry the same revision until the agent installs it again.
  • AMCL's first_map_only: false is now pinned in nav2_params.yaml.
  • The agent no longer logs (restart nav to load it).

Health contract

map.revision is now what map_server serves. Two nullable fields are added without a version bump:

  • map.installed: the symlink.
  • map.error: why the last load failed.

With no reloader running, revision equals installed. Updated in health.schema.json, control-plane.md and protocol.health.

Cost

One process and one DDS participant, taking the robot stack to about 27 of 33 (not measured with dds-check).

Known limits

  • site use-map <rev> still needs a nav restart; it tells nobody.
  • If map_reloader alone respawns while a deferred load is pending, it reads the symlink at startup and reports the installed revision although map_server still serves the old one. map_server offers no way to ask what it is serving.

Verification

  • mote_bringup/test/test_map_reloader.py: 11 tests against a fake map_server, lane and AMCL. Covers load and pose carry, deferral, no task server, failed load and no retry, another floor, and map_server absent.
  • mote_tasks/test/test_map_reload_lane.py: the real task server and the real reloader. A promotion during a held Nav2 goal does not load until the mission succeeds, then loads and brings the new zones. An idle robot loads at once and a goto to the new zone succeeds. A mission sent mid-load is refused as busy.
  • mote_fleet/test/test_agent.py: health names the old revision between install and load and the new one after; a failed load is reported; map/installed is published.
  • mote_fleet/test/test_e2e_map_registry.py, new sibling test: real broker, fleet server, fleetctl promote, agent and reloader. The lane is held busy across the promotion, so MQTT health shows installed new and revision old; releasing it loads the new map and health follows.
  • Sim acceptance, pixi run sim-map-reload-test (new): mote_world with a second revision that has a 4×4 px obstacle drawn in, promoted while sim-nav is up and idle.
    STEP2 OK: goto kitchen succeeded; pose (1.870, 1.931, 16.2 deg); /map a68d05b0267e
    STEP3 OK: serving 20990101T000000 and /map now 86c5d4bcdc83 (0.04s after map/installed, no restart)
           pose after (1.876, 1.933, 17.0 deg): off by 0.007 m, 0.8 deg
    STEP5 OK: goto pickup succeeded on 20990101T000000
    MAP RELOAD TEST PASS
    
  • The mote_bringup, mote_fleet and mote_tasks suites (dev env): 830 passed, 11 skipped. Pre-commit is clean.
  • Not run on the Pi.

🤖 Generated with Claude Code

map_server reads its map once, at startup. A promoted revision was pulled and
installed by the agent and then ignored until someone restarted nav; health,
reading the floor's symlink, meanwhile reported the new revision (mote-01,
2026-09-21).

map_reloader (mote_bringup), started by nav2_launch.py with localisation,
hears map/installed from the agent, asks the task server's task/idle whether
the lane is free, calls map_server/load_map with the revision's own map.yaml,
re-seeds AMCL on /initialpose from the last amcl_pose, and reports
map/serving. A mission in flight defers the load to its terminal state; a load
in progress makes the task server refuse new missions as busy; a failed load
leaves the old map served and says why. The task server reloads the floor's
zones when the served revision changes.

Health's map.revision is now what map_server serves, with map.installed (the
symlink) and map.error beside it. AMCL's first_map_only is pinned false.

Tests: reloader unit tests against fake map_server/lane/AMCL; the real task
server and reloader deferring a load across a held Nav2 goal; agent health
between install and load; an e2e promote -> pull -> load over a real broker;
and a sim acceptance (pixi run sim-map-reload-test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M93DeThw4ndhxsg4Dho6XP
@MJohnson459
MJohnson459 merged commit 7ad7b77 into main Sep 29, 2026
6 checks passed
@MJohnson459
MJohnson459 deleted the map-hot-reload branch September 29, 2026 19:58
MJohnson459 pushed a commit that referenced this pull request Sep 29, 2026
#120 added a 330-word 'Fleet: loading a promoted map' section to the old
file. Its narrative already lives in docs/fleet/README.md §11; the rules
it carried become bullets under Map registry. The 'next bringup' line
and the DDS total (~27) are corrected, and a short 'Editing this file'
rule says what belongs here so the next feature adds bullets, not a
section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Ly1jZnCmZq3aaa2iKM9Zz
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