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 by rbnx boot. Consumers (scene, nav) bind the contracts, not the
SLAM engine.
- Capability surface, config schema, and persistence layout: CAPABILITY.md.
- Provider instance configuration reference: config.spec.
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 |
fastlio2 |
broken (drift) — repro only | — |
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. - Pick a deployment target and reference the matching package manifest
from your deploy
robonix_manifest.yaml:
service:
- name: mapping
url: https://github.com/syswonder/service-map-rbnx
# manifest: package_manifest.jetson-native.yaml # x86+docker is default
config:
sensor_providers:
lidar3d: roof_lidar
rgb: front_camera
depth: front_camera
odom: base_chassis
occupancy_sources: [lidar]
deskew_lidar: true
params_file: config/rtabmap_params.yaml
rtabmap_params:
Grid/FootprintLength: 0.84
Grid/FootprintWidth: 0.60
rbnx build -f robonix_manifest.yaml then rbnx boot -f robonix_manifest.yaml.
4. 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.
Saving & re-using a map
Mapping starts with a fresh runtime database. Call save_map(map_id) after
coverage is complete; the named map lives under {MAPPING_MAPS_DIR}/{map_id}/
(default: the package's maps/ dir, which survives container restarts):
maps/lab_3f/rtabmap.db occupancy.pgm occupancy.yaml occupancy.png cloud.pcd meta.yaml
- Build a map:
map_mode: mapping. Drive the robot around, then callsave_map(map_id)to publish an immutable database and previews. - Re-use a map: set
map_idplusmap_mode: localization. Mapping copies the saved db to a private runtime path and re-localizes against it; the map frame is stable across restarts, so Scene can load semantic state for the same id. - Start fresh:
map_mode: mapping(the default). It never writes an existing saved map.
Localization-mode persistence only re-anchors correctly because the map frame is loaded from the saved db. Without
map_mode: localizationthe map origin resets to the robot's boot pose each run.
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(writesrtabmap.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 current mode.
- 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).
- Click the map → pose estimate — seeds
/initialposeso rtabmap re-localizes; the activity log records the seeded pose and, a few seconds later, where it converged + the distance from your estimate.
These are the same operations exposed as runtime RPC + MCP capabilities
(so Pilot can drive them too): save_map, load_map, pose_estimate,
switch_mode (the webui adds reset + delete on top). All work on the
running rtabmap without a redeploy — load/switch_mode call rtabmap's
runtime services and fall back to a restart with the config's map_mode /
map_id when those services aren't reachable.
The web UI has no auth — it's a LAN debug tool. Don't expose the port to an untrusted network.
Layout
mapping_rbnx/
├── package_manifest.yaml x86+docker (default)
├── package_manifest.jetson-docker.yaml arm64 Jetson + docker
├── package_manifest.jetson-native.yaml arm64 Jetson + host ROS2
├── CAPABILITY.md capability surface + config spec
├── src/mapping_rbnx/atlas_bridge.py cap registration, sensor discovery, persistence
├── src/mapping_rbnx/map_to_odom_bridge.py optional split-odometry TF bridge
├── src/mapping_rbnx/odom_bridge_math.py planar transform and interpolation helpers
├── launch/rtabmap_2d.launch.py sensor-agnostic rtabmap launch
├── scripts/
│ ├── build.sh per-target build
│ ├── build_ros2_overlay.sh isolated map interface build
│ ├── start.sh native↔docker dispatch
│ ├── start_engine.sh in-container SLAM launcher
│ ├── start_native.sh host-process launcher
│ └── save_map.py offline map snapshot (pgm/png/pcd/meta)
└── docker/ Dockerfile, Dockerfile.jetson, compose
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.- Map origin drifts between runs — you're in
mappingmode (origin = boot pose). Uselocalizationto re-anchor to the saved map.
License: MulanPSL-2.0