SLAM mapping service for Robonix. It
turns a robot’s lidar / camera / odom streams into a live 2D occupancy
grid, a 3D point cloud, and a SLAM-corrected pose, published under a
fixed, engine-agnostic capability surface (robonix/service/map/*), and
persists named maps so a robot can re-localize across restarts.
It is a Robonix service package: it registers with atlas, discovers its
sensor inputs by capability contract (never hardcoded topics), and is brought
up by rbnx boot. Consumers (scene, nav) bind the contracts, not the
SLAM engine.
Occupancy grid editor: edit saved grids as reversible deltas; keep the SLAM pose graph unchanged.
Capability surface, config schema, and persistence layout: CAPABILITY.md.
Offline global-localization benchmark, dataset setup, and results: BENCHMARK.md.
SLAM engines (algo)
algo
use
inputs
rtabmap(default, recommended)
sim + real; 2D/3D/RGBD, sensor-agnostic
any of lidar2d / lidar3d / rgbd (+ odom)
dlio
real-robot 3D Livox + IMU
lidar3d + imu, needs a colcon ws at /ws/install
slam_toolbox
2D lidar only; CPU-only, no database, pose graph serializes to two files
lidar2d (+ odom via tf)
fastlio2
broken (drift) — repro only
—
Saved maps are engine-tagged. save_map records the engine in the map’s
meta.yaml, list_maps reports it, and load_map refuses a map built by a
different engine by name instead of failing somewhere inside the load. The
directory layout does not change with the engine — occupancy.{pgm,yaml,png},
cloud.pcd, meta.yaml plus that engine’s graph (rtabmap.db, or
posegraph.posegraph + posegraph.data) — so scene, the web UI and every other
consumer read one shape. The web UI names the running backend in its header and
greys out library entries built by another one.
Tuning slam_toolbox
slam_toolbox_params is the counterpart of rtabmap_params. The defaults suit
a slow indoor platform in a room-scale map; an unknown key fails at init.
key
default
what it does
min_travel_m
0.1
distance before a new pose-graph node
min_heading_rad
0.1
rotation before a new pose-graph node
scan_buffer
30
recent scans matched against each other (scan_buffer x min_travel_m should stay near the room scale)
loop_search_m
2.5
radius searched for a loop closure
Localization engines (localizer)
Building a map and localizing in a saved one are separate jobs, and the second
one is where “click 2D Pose Estimate, then click again” comes from: a scan
matcher can only refine a guess it is already given. A particle filter over the
saved occupancy grid can start from no guess at all. This applies to
slam_toolbox; RTAB-Map relocalizes against its own database and ignores
localizer.
The localizer never publishes map → odom (tf_broadcast: false): one node
owns that frame at every instant, which is the SLAM engine. load_map uses the
localizer as a relocalization plugin, not a second localizer: the SLAM engine is held, the filter is started and told to scatter
its particles, and as soon as it converges its pose is handed to the engine
(activate at that pose) and the filter is stopped. Exactly one node publishes
map → odom when that returns — the engine — so the deployment can still map,
switch mode and load again, and an engine added later plugs into the same three
calls (hold, activate(pose), resume).
localizer
what load_map does
cost
none(default)
the SLAM engine localizes in its own map; a pose seed is effectively required
engine-dependent
amcl
map_server serves the saved grid, nav2_amcl owns map → odom; no initial pose → global localization (particles over the whole map)
CPU only, tens of MB
beluga
same interfaces via beluga_amcl (drop-in), plus NDT variants
same
service:
- name: mapping
config:
algo: slam_toolbox # mapping still runs on the SLAM engine
localizer: amcl # localization on saved maps, global relocalization
localizer_particles: {min: 500, max: 2000}
For slam_toolbox, get_pose_candidates exposes scan-matched Top-K positions
from the current localization run to Scene and other clients when the match
is low-scoring or ambiguous. Startup and Relocalize use the same controller:
one scan search, then either a clear automatic pose or up to five cached
candidates with AMCL/Beluga running alongside operator selection. Both paths
verify the live laser before the map engine takes over. Candidate queries
only read the cached result; they never repeat the global search. Relocalize
cancels the previous run before starting a fresh search. The
operation rejects other engines; RTAB-Map keeps its existing /initialpose
path and does not use this candidate algorithm. Already confirmed localization
returns no candidates. A confident automatic
match returns an empty list and requires no operator choice. The mapping Web
UI shows ambiguous candidates as numbered arrows; choosing one submits it
through the existing pose_estimate service. Check the reported map_id and
generation against the current map before using a candidate, then wait for
localization and live scan verification. pose_estimate also accepts a manual
guess when needed.
The launch branches on the provider roles bound by the deployment, so the same
service supports 2D lidar, 3D lidar, RGB-D, and external odometry without
robot-specific branches.
How to integrate it on your robot
Register your sensors as Robonix primitives under the standard
contracts (robonix/primitive/lidar/lidar3d, .../camera/depth,
.../chassis/odom, …). mapping discovers them via atlas.
A 3-D lidar may instead be projected by service-pcld2lscan-rbnx. Bind its
output with sensor_providers.scan_converter; slam_toolbox and particle
filter localizers then consume the resolved LaserScan topic.
Pick a deployment target and reference the matching package manifest
from your deploy robonix_manifest.yaml:
rbnx build -f robonix_manifest.yaml then rbnx boot -f robonix_manifest.yaml.
Consume the map: subscribe to robonix/service/map/occupancy_grid /
.../pointcloud / .../pose (resolve them via atlas).
config/rtabmap_params.template.yaml is
only a starting template. Copy it into the robot deployment repository and set
params_file; Mapping never loads the upstream template at runtime. Inline
rtabmap_params applies after the deploy-owned file.
With external odometry, deskew_lidar compensates each PointCloud2 point in
the odom frame before SLAM. Bind only the sensor roles Mapping should consume.
Separate localization and navigation odometry
Some robots need accurate ICP/RGB-D odometry for RTAB-Map localization but a
lower-latency chassis odometry stream for navigation. Enable the optional
split-odometry bridge for this setup:
service:
- name: mapping
url: https://github.com/syswonder/service-map-rbnx
config:
algo: rtabmap
base_frame: base_link
# Private RTAB-Map odometry frame.
odom_frame: odom_icp
# Public chassis odometry used by navigation.
navigation_odom_bridge: true
navigation_odom_topic: /odom
navigation_odom_frame: odom
sensor_providers:
lidar3d: roof_lidar
# Do not bind odom in split-odometry mode.
In this mode, RTAB-Map uses an internal message-only odometry trajectory in
odom_icp, the chassis owns odom -> base_link, and Mapping publishes the
correction required by navigation:
map -> odom -> base_link
The bridge computes map -> odom from RTAB-Map localization and the two
timestamp-aligned odometry poses. odom_frame and navigation_odom_frame
must be different, and sensor_providers.odom must not be configured.
The feature defaults to false; existing external- and internal-odometry
deployments are unchanged. See config.spec for the complete
field definitions.
Deployment targets
One package, three targets (selected by the deploy manifest: field — see
CAPABILITY.md):
target
manifest
runtime
x86_64 + docker
package_manifest.yaml
docker (docker/Dockerfile)
arm64 Jetson + docker
package_manifest.jetson-docker.yaml
docker (docker/Dockerfile.jetson, L4T)
arm64 Jetson + native
package_manifest.jetson-native.yaml
host ROS2 (scripts/start_native.sh)
Add a target by adding a package_manifest.<target>.yaml plus a case branch
in scripts/build.sh — the rest of the package is unchanged.
The generated ROS 2 overlay intentionally builds only Robonix’s custom map
interface package. Standard interfaces such as sensor_msgs continue to come
from the target’s ROS 2 Humble installation, preserving its support libraries
and CMake exports for consumers such as cv_bridge.
Maps, modes and session state
Three databases, kept apart
RTAB-Map never writes a saved map. Knowing which file is which explains every
rule below.
map_ops keeps one record of which database is live and hands it to
save_map. Every entry point — deployment config, gRPC, MCP, the web UI —
updates the same record, so a map saved after a load snapshots the database
RTAB-Map actually holds open.
Startup configuration
map_mode and map_id in the deployment config choose the startup state.
There are two useful combinations:
# Build a new map. This is the default: omit both keys and you get it.
mapping:
config: {}
# Come up localized on a saved map — the stable-frame form used for tasks.
mapping:
config:
map_mode: localization
map_id: lab_3f
map_mode defaults to mapping, which always opens a fresh, empty
runtime database. A map_id given alongside it is ignored: it names a saved
artifact, not a live session, and mapping never reopens one. There is no
“start up and keep extending map X” configuration.
map_mode: localization requires map_id, and fails to start if that map
is missing — deliberately, rather than silently mapping from the boot pose.
It copies the saved database to a runtime path and localizes against the
copy, so the map frame is stable across restarts and Scene can load
semantic state for the same id.
reset_map: true is only meaningful in mapping mode.
Runtime operations
operation
changes the database
changes the mode
changes the frame
save_map(map_id)
no — snapshots the live database into a new saved map
no
no
load_map(map_id)
yes — copies the saved map and switches onto the copy
yes, to localization
yes, to the loaded map’s frame
switch_mode(mode)
no
yes
no
reset_map
no — clears working memory, the file stays
back to mapping
yes — origin becomes the robot’s current pose
pose_estimate(x, y, θ)
no
no
no
delete_map(map_id)
removes a saved map from disk
no
no
Notes that matter in the field:
save_map publishes once. Saving under an existing map_id is refused;
a corrected map goes under a new id.
load_map always localizes. A mapping mode argument is accepted and
coerced, because RTAB-Map only restores the saved occupancy grid when the
database is opened for localization.
reset_map invalidates coordinates. The rebuilt map does not share the
old frame, so positions recorded against the previous map are stale. The
lifecycle broadcast bumps its generation to say so.
Loading replaces the live session. Anything mapped since the last save
is gone; save first.
Switching mode at runtime
The config’s map_mode is only the startup default. switch_mode flips the
running RTAB-Map without touching the database or the frame.
Prefer a restart over a runtime switch. Going from localization back to
mapping is the risky direction, and the loaded map usually leaves the live view
when you do it. The web UI shows a standing warning while localized and asks
for confirmation before that switch. To build a new map, restart the service
with map_mode: mapping instead.
Why the loaded map disappears
Nothing is deleted — the map becomes a graph component the published map is
not assembled from. Four steps, all in RTAB-Map 0.23.x:
Entering localization calls Memory::incrementMapId(), which opens a new
session id and flushes short-term memory. Every load does this, because a
load always enters localization.
While localized, each new node is dropped again rather than kept
(moveToTrash(_lastSignature, …)), so the session id stays put.
Switching back to mapping only flips Mem/IncrementalMemory to true.
Memory::addSignatureToStm links a new node to the previous one only
when their session ids match, so the first node built after the switch
gets no odometry link back into the loaded map. The graph now has two
disconnected components.
The published map comes from Rtabmap::optimizeCurrentMap, which optimizes
the connected component around the current node. The loaded map is in the
other component, so it is not in /map.
It comes back when RTAB-Map detects a loop closure between the two sessions:
that link joins the components and the whole map returns. So the switch is
only safe where relocalization can actually succeed. The database on disk is
never affected either way.
Workflows
Build the first map — start with no map_id / map_mode, drive the
space, save_map("lab_3f").
Build another map — restart the service, then drive and save under a new
id. Do not load an existing map first.
Run tasks on a saved map — start with map_mode: localization and
map_id, or load_map(id) on a running service.
Correct a saved map — build a fresh session and save under a new id; a
published map is immutable.
RTAB-Map UI
The RTAB-Map viewer starts with every mapping session so the graph, loop
closures and per-node grids are visible while the robot drives. It needs an X
server: scripts/start.sh forwards the host DISPLAY into the container, and
a session without one logs a line and continues headless. Set
MAPPING_ENABLE_VIZ=false to keep it off on a robot that has a display but no
operator.
Web UI (live map + runtime map ops)
A dependency-light operator page (stdlib http.server + Pillow) is enabled on
port 8091 by default; set deployment config webui_port: 0 to disable it.
It binds 127.0.0.1 by default because the map controls are unauthenticated.
An authenticated overlay deployment may explicitly set webui_host (or
MAPPING_WEBUI_HOST); otherwise use the local browser or an SSH tunnel.
It runs inside the mapping bridge process, so its buttons call the same
map_ops impls the gRPC/MCP capabilities use — no extra round trip — and it
reads the live /map + pose straight off the bridge’s rclpy node.
Live map canvas — occupancy grid + robot pose, with drag-to-pan,
wheel-zoom, a 1 m grid, and double-click-to-fit. Same world-centered
view model as scene’s web UI (canvas backing-store pinned to display size,
so click coordinates are exact).
Save — snapshot the live map under a map_id (writes
rtabmap.db + occupancy.png/pgm/yaml + meta.yaml).
Library — every saved map with a thumbnail; Load re-localizes onto
it, Del removes it from disk.
Mode — flip Mapping ⇄ Localization at runtime; a badge + button
highlight shows the mode the service reports, so a mode changed by config,
MCP, a load or a reset shows up here too. Localization ⇒ mapping raises a
warning first (see Switching mode at runtime).
Reset map — wipe the live SLAM session and rebuild from scratch (for
when mapping diverges). Note: the origin resets to the robot’s current
pose, so the rebuilt frame won’t match the old map (origin drift).
Set pose estimate — arm the button, then press where the robot is and
drag the way it faces. The heading matters as much as the position: seeding
the right spot facing backwards fails to relocalize just as a wrong spot
does. The activity log records the seeded pose and, a few seconds later,
where it converged and how far that is from your estimate.
Live lidar overlay — the current range returns are drawn on the map in
green (2-D scan) and blue (point cloud). This is the check that answers “is
localization right”: if the returns do not sit on the walls of the map, the
pose is wrong. Topics come from whatever Atlas resolved for
robonix/primitive/lidar/lidar and robonix/primitive/lidar/lidar3d, so the
overlay follows the deployment’s capability bindings. When no 2-D scan
capability is bound the page looks for a LaserScan on the graph instead and
says which one it picked — a robot whose scan is projected downstream from a
3-D cloud (and therefore never declared) still gets its overlay. Pin one with
webui_scan_topic in the deployment config, or MAPPING_WEBUI_SCAN_TOPIC.
This overlay follows the deployment’s capability bindings — a Webots TIAGo shows
its 2-D scan, a Ranger with a mid360 shows its cloud, and a deployment with
no lidar bound simply has no overlay. Cloud returns are limited to a band
around the sensor plane (MAPPING_WEBUI_CLOUD_Z_BAND, default 0.35 m) so
they can be compared against a 2-D grid, and both are subsampled to
MAPPING_WEBUI_MAX_POINTS (default 1200).
These are the same operations exposed as runtime RPC + MCP capabilities
(so Pilot can drive them too): save_map, load_map, get_pose_candidates, pose_estimate,
switch_mode (the webui adds reset + delete on top).
get_pose_candidates is slam_toolbox-only; RTAB-Map rejects that query and
keeps its existing database localization and /initialpose behaviour. The
remaining operations use the selected engine’s runtime implementation without
a redeploy.
The web UI has no auth — it’s a LAN debug tool. Don’t expose the port to an
untrusted network.
/map never populates — a provider binding is missing or points to the
wrong provider. Check the [start_engine] rtabmap scan2d=… scan3d=… line.
map_mode=localization errors “no saved map” — run a mapping session
with that map_id first, and confirm MAPPING_MAPS_DIR is the same path
(mounted) across runs.
Map origin drifts between runs — you’re in mapping mode (origin =
boot pose). Use localization to re-anchor to the saved map.
save_map says “no live rtabmap database found to snapshot” — nothing
has opened a database yet, or the recorded one was removed. The message
lists the paths it tried. A service that has been running and mapping always
has one; if this appears right after a load, the deployment predates the
shared live-database record and should be updated.
The map “disappeared” after switching to mapping mode — expected, see
Why the loaded map disappears. It returns on a loop closure with the loaded
session; the saved map on disk is intact either way. Build new maps from a
restart instead of a runtime switch.
mapping_rbnx
SLAM mapping service for Robonix. It turns a robot’s lidar / camera / odom streams into a live 2D occupancy grid, a 3D point cloud, and a SLAM-corrected pose, published under a fixed, engine-agnostic capability surface (
robonix/service/map/*), and persists named maps so a robot can re-localize across restarts.It is a Robonix service package: it registers with
atlas, discovers its sensor inputs by capability contract (never hardcoded topics), and is brought up byrbnx boot. Consumers (scene,nav) bind the contracts, not the SLAM engine.SLAM engines (
algo)rtabmap(default, recommended)dlio/ws/installslam_toolboxfastlio2Saved maps are engine-tagged.
save_maprecords the engine in the map’smeta.yaml,list_mapsreports it, andload_maprefuses a map built by a different engine by name instead of failing somewhere inside the load. The directory layout does not change with the engine —occupancy.{pgm,yaml,png},cloud.pcd,meta.yamlplus that engine’s graph (rtabmap.db, orposegraph.posegraph+posegraph.data) — so scene, the web UI and every other consumer read one shape. The web UI names the running backend in its header and greys out library entries built by another one.Tuning
slam_toolboxslam_toolbox_paramsis the counterpart ofrtabmap_params. The defaults suit a slow indoor platform in a room-scale map; an unknown key fails at init.min_travel_m0.1min_heading_rad0.1scan_buffer30scan_buffer x min_travel_mshould stay near the room scale)loop_search_m2.5Localization engines (
localizer)Building a map and localizing in a saved one are separate jobs, and the second one is where “click 2D Pose Estimate, then click again” comes from: a scan matcher can only refine a guess it is already given. A particle filter over the saved occupancy grid can start from no guess at all. This applies to slam_toolbox; RTAB-Map relocalizes against its own database and ignores
localizer.The localizer never publishes
map → odom(tf_broadcast: false): one node owns that frame at every instant, which is the SLAM engine.load_mapuses the localizer as a relocalization plugin, not a second localizer: the SLAM engine is held, the filter is started and told to scatter its particles, and as soon as it converges its pose is handed to the engine (activateat that pose) and the filter is stopped. Exactly one node publishesmap → odomwhen that returns — the engine — so the deployment can still map, switch mode and load again, and an engine added later plugs into the same three calls (hold,activate(pose),resume).load_mapdoesnone(default)amclmap_serverserves the saved grid,nav2_amclownsmap → odom; no initial pose → global localization (particles over the whole map)belugabeluga_amcl(drop-in), plus NDT variantsFor
slam_toolbox,get_pose_candidatesexposes scan-matched Top-K positions from the current localization run to Scene and other clients when the match is low-scoring or ambiguous. Startup and Relocalize use the same controller: one scan search, then either a clear automatic pose or up to five cached candidates with AMCL/Beluga running alongside operator selection. Both paths verify the live laser before the map engine takes over. Candidate queries only read the cached result; they never repeat the global search. Relocalize cancels the previous run before starting a fresh search. The operation rejects other engines; RTAB-Map keeps its existing/initialposepath and does not use this candidate algorithm. Already confirmed localization returns no candidates. A confident automatic match returns an empty list and requires no operator choice. The mapping Web UI shows ambiguous candidates as numbered arrows; choosing one submits it through the existingpose_estimateservice. Check the reportedmap_idandgenerationagainst the current map before using a candidate, then wait for localization and live scan verification.pose_estimatealso accepts a manual guess when needed.The launch branches on the provider roles bound by the deployment, so the same service supports 2D lidar, 3D lidar, RGB-D, and external odometry without robot-specific branches.
How to integrate it on your robot
Register your sensors as Robonix primitives under the standard contracts (
robonix/primitive/lidar/lidar3d,.../camera/depth,.../chassis/odom, …). mapping discovers them via atlas.A 3-D lidar may instead be projected by
service-pcld2lscan-rbnx. Bind its output withsensor_providers.scan_converter;slam_toolboxand particle filter localizers then consume the resolved LaserScan topic.Pick a deployment target and reference the matching package manifest from your deploy
robonix_manifest.yaml:rbnx build -f robonix_manifest.yamlthenrbnx boot -f robonix_manifest.yaml.Consume the map: subscribe to
robonix/service/map/occupancy_grid/.../pointcloud/.../pose(resolve them via atlas).config/rtabmap_params.template.yamlis only a starting template. Copy it into the robot deployment repository and setparams_file; Mapping never loads the upstream template at runtime. Inlinertabmap_paramsapplies after the deploy-owned file.With external odometry,
deskew_lidarcompensates each PointCloud2 point in the odom frame before SLAM. Bind only the sensor roles Mapping should consume.Separate localization and navigation odometry
Some robots need accurate ICP/RGB-D odometry for RTAB-Map localization but a lower-latency chassis odometry stream for navigation. Enable the optional split-odometry bridge for this setup:
In this mode, RTAB-Map uses an internal message-only odometry trajectory in
odom_icp, the chassis ownsodom -> base_link, and Mapping publishes the correction required by navigation:The bridge computes
map -> odomfrom RTAB-Map localization and the two timestamp-aligned odometry poses.odom_frameandnavigation_odom_framemust be different, andsensor_providers.odommust not be configured.The feature defaults to
false; existing external- and internal-odometry deployments are unchanged. See config.spec for the complete field definitions.Deployment targets
One package, three targets (selected by the deploy
manifest:field — see CAPABILITY.md):package_manifest.yamldocker/Dockerfile)package_manifest.jetson-docker.yamldocker/Dockerfile.jetson, L4T)package_manifest.jetson-native.yamlscripts/start_native.sh)Add a target by adding a
package_manifest.<target>.yamlplus a case branch inscripts/build.sh— the rest of the package is unchanged.The generated ROS 2 overlay intentionally builds only Robonix’s custom
mapinterface package. Standard interfaces such assensor_msgscontinue to come from the target’s ROS 2 Humble installation, preserving its support libraries and CMake exports for consumers such ascv_bridge.Maps, modes and session state
Three databases, kept apart
RTAB-Map never writes a saved map. Knowing which file is which explains every rule below.
{MAPPING_MAPS_DIR}/{map_id}/rtabmap.dbsave_maponly, once — immutable afterwards{MAPPING_RUNTIME_DB_DIR}/…(default/tmp/robonix-mapping-runtime)~/.ros/rtabmap.dbsave_mapA saved map directory holds the artifacts alongside the database:
map_opskeeps one record of which database is live and hands it tosave_map. Every entry point — deployment config, gRPC, MCP, the web UI — updates the same record, so a map saved after a load snapshots the database RTAB-Map actually holds open.Startup configuration
map_modeandmap_idin the deployment config choose the startup state. There are two useful combinations:map_modedefaults tomapping, which always opens a fresh, empty runtime database. Amap_idgiven alongside it is ignored: it names a saved artifact, not a live session, and mapping never reopens one. There is no “start up and keep extending map X” configuration.map_mode: localizationrequiresmap_id, and fails to start if that map is missing — deliberately, rather than silently mapping from the boot pose. It copies the saved database to a runtime path and localizes against the copy, so the map frame is stable across restarts and Scene can load semantic state for the same id.reset_map: trueis only meaningful in mapping mode.Runtime operations
save_map(map_id)load_map(map_id)switch_mode(mode)reset_mappose_estimate(x, y, θ)delete_map(map_id)Notes that matter in the field:
save_mappublishes once. Saving under an existingmap_idis refused; a corrected map goes under a new id.load_mapalways localizes. Amappingmode argument is accepted and coerced, because RTAB-Map only restores the saved occupancy grid when the database is opened for localization.reset_mapinvalidates coordinates. The rebuilt map does not share the old frame, so positions recorded against the previous map are stale. The lifecycle broadcast bumps its generation to say so.Switching mode at runtime
The config’s
map_modeis only the startup default.switch_modeflips the running RTAB-Map without touching the database or the frame.Prefer a restart over a runtime switch. Going from localization back to mapping is the risky direction, and the loaded map usually leaves the live view when you do it. The web UI shows a standing warning while localized and asks for confirmation before that switch. To build a new map, restart the service with
map_mode: mappinginstead.Why the loaded map disappears
Nothing is deleted — the map becomes a graph component the published map is not assembled from. Four steps, all in RTAB-Map 0.23.x:
Memory::incrementMapId(), which opens a new session id and flushes short-term memory. Every load does this, because a load always enters localization.moveToTrash(_lastSignature, …)), so the session id stays put.Mem/IncrementalMemoryto true.Memory::addSignatureToStmlinks a new node to the previous one only when their session ids match, so the first node built after the switch gets no odometry link back into the loaded map. The graph now has two disconnected components.Rtabmap::optimizeCurrentMap, which optimizes the connected component around the current node. The loaded map is in the other component, so it is not in/map.It comes back when RTAB-Map detects a loop closure between the two sessions: that link joins the components and the whole map returns. So the switch is only safe where relocalization can actually succeed. The database on disk is never affected either way.
Workflows
map_id/map_mode, drive the space,save_map("lab_3f").map_mode: localizationandmap_id, orload_map(id)on a running service.RTAB-Map UI
The RTAB-Map viewer starts with every mapping session so the graph, loop closures and per-node grids are visible while the robot drives. It needs an X server:
scripts/start.shforwards the hostDISPLAYinto the container, and a session without one logs a line and continues headless. SetMAPPING_ENABLE_VIZ=falseto keep it off on a robot that has a display but no operator.Web UI (live map + runtime map ops)
A dependency-light operator page (stdlib
http.server+ Pillow) is enabled on port8091by default; set deployment configwebui_port: 0to disable it. It binds127.0.0.1by default because the map controls are unauthenticated. An authenticated overlay deployment may explicitly setwebui_host(orMAPPING_WEBUI_HOST); otherwise use the local browser or an SSH tunnel.It runs inside the mapping bridge process, so its buttons call the same
map_opsimpls the gRPC/MCP capabilities use — no extra round trip — and it reads the live/map+ pose straight off the bridge’s rclpy node.map_id(writesrtabmap.db+occupancy.png/pgm/yaml+meta.yaml).robonix/primitive/lidar/lidarandrobonix/primitive/lidar/lidar3d, so the overlay follows the deployment’s capability bindings. When no 2-D scan capability is bound the page looks for aLaserScanon the graph instead and says which one it picked — a robot whose scan is projected downstream from a 3-D cloud (and therefore never declared) still gets its overlay. Pin one withwebui_scan_topicin the deployment config, orMAPPING_WEBUI_SCAN_TOPIC. This overlay follows the deployment’s capability bindings — a Webots TIAGo shows its 2-D scan, a Ranger with a mid360 shows its cloud, and a deployment with no lidar bound simply has no overlay. Cloud returns are limited to a band around the sensor plane (MAPPING_WEBUI_CLOUD_Z_BAND, default 0.35 m) so they can be compared against a 2-D grid, and both are subsampled toMAPPING_WEBUI_MAX_POINTS(default 1200).These are the same operations exposed as runtime RPC + MCP capabilities (so Pilot can drive them too):
save_map,load_map,get_pose_candidates,pose_estimate,switch_mode(the webui addsreset+deleteon top).get_pose_candidatesis slam_toolbox-only; RTAB-Map rejects that query and keeps its existing database localization and/initialposebehaviour. The remaining operations use the selected engine’s runtime implementation without a redeploy.Layout
Troubleshooting
/mapnever populates — a provider binding is missing or points to the wrong provider. Check the[start_engine] rtabmap scan2d=… scan3d=…line.map_mode=localizationerrors “no saved map” — run amappingsession with thatmap_idfirst, and confirmMAPPING_MAPS_DIRis the same path (mounted) across runs.mappingmode (origin = boot pose). Uselocalizationto re-anchor to the saved map.save_mapsays “no live rtabmap database found to snapshot” — nothing has opened a database yet, or the recorded one was removed. The message lists the paths it tried. A service that has been running and mapping always has one; if this appears right after a load, the deployment predates the shared live-database record and should be updated.License: MulanPSL-2.0