nav2_wrapper_rbnx
Robonix service package that wraps a standard Nav2
stack and owns the robonix/service/navigation/* capability. It routes the
Robonix gRPC/MCP contracts (navigate, navigate/status, navigate/cancel) onto Nav2's
navigate_to_pose action, and discovers every topic it consumes (/map,
/odom, /scan) through atlas — so the same package drops onto any body that
publishes those contracts, in simulation or on real hardware.
It is body- and scene-agnostic: nothing in the package names a specific robot, topic, or provider. The deploy manifest picks a params profile and a build target; everything else is resolved at runtime.
Capability surface
| Contract | Transport | Handler |
|---|---|---|
robonix/service/navigation/driver |
gRPC | Driver(CMD_INIT/SHUTDOWN, …) — lifecycle (Service) |
robonix/service/navigation/navigate |
gRPC + MCP | Navigate(PoseStamped) → dispatches a Nav2 action goal |
robonix/service/navigation/navigate/status |
gRPC + MCP | GetNavigationStatus(run_id) → status from cache |
robonix/service/navigation/navigate/cancel |
gRPC + MCP | CancelNavigation(run_id) → Nav2 cancel_goal_async |
The driver + the three data contracts are hosted and declared on atlas by
robonix_api.Service.run(). navigate / status
/ cancel are live from process start; each guards on the rclpy node, so a call
that lands before CMD_INIT finishes returns a clean "not initialized" instead
of crashing.
Lifecycle (how a deploy brings nav2 up)
The package is a robonix_api.Service. scripts/start.sh launches the bridge
(in a container or natively — see Deployment targets); the bridge registers
nav2 with atlas, serves the Driver gRPC, and blocks awaiting
Driver(CMD_INIT, config_json) from rbnx boot. There is no config on disk or
in the environment — the config dict arrives only over this gRPC channel.
On CMD_INIT the on_init handler:
- Resolves upstream deps from atlas by contract (
ATLAS.find_capability), never by hardcoded topic name. If a required dep is missing it returnsDeferred(...)— rbnx retries once the upstream provider registers, so nav2 is never spawned half-wired. - Spawns
ros2 launch nav2_bringup navigation_launch.pywith the selected params profile,use_sim_time, and the resolved topics passed as launch remaps. - Brings up an rclpy node +
navigate_to_poseActionClient and waits (action_wait_s) for the Nav2 lifecycle to advertise the action server.
CMD_SHUTDOWN (or a process signal) tears the Nav2 subprocess down via
on_shutdown so it never outlives the wrapper.
Quick start (Webots simulation)
A minimal deploy that proves nav2 navigates the Webots Tiago lives in the
robonix repo at examples/webots/nav2_test.yaml (atlas + executor + chassis +
lidar + mapping + nav2). With the Webots sim already running:
# 1. one-time: build the nav2 docker image (x86 + docker is the default target)
rbnx build -p /path/to/nav2_wrapper_rbnx # builds image `robonix-nav2`
# 2. boot the stack
cd <robonix>/examples/webots
rbnx boot -f nav2_test.yaml # nav2 reaches ACTIVE
# 3. send a goal over the navigate gRPC and watch /cmd_vel move the robot
# (goal in the map frame for the sim profile; see Params profiles)
The sim params profile navigates in the map frame with online SLAM: an
rtabmap mapping provider publishes both a live /map and a map→odom TF. Both
costmaps anchor in map and a StaticLayer tracks /map as it grows
(map_subscribe_transient_local), so the robot maps and navigates at the same
time — obstacles populate the costmaps straight from the SLAM grid, and the
ObstacleLayer adds anything not yet mapped. It needs a SLAM provider that
owns map→odom (rtabmap in the Webots example); no AMCL or pre-built map.
Deployment targets
Nav2 itself is large and the upstream apt packages build cleanly on Humble, so
this package vendors only the YAML params in config/. Three targets cover the
common deployments; the target is selected by RBNX_BUILD_TARGET (build) and the
matching per-target package manifest (rbnx boot -f … --manifest):
RBNX_BUILD_TARGET |
package manifest | Where Nav2 runs |
|---|---|---|
x86-docker (default) |
package_manifest.yaml |
x86_64 host, Nav2 in the robonix-nav2 image |
jetson-docker |
package_manifest.jetson-docker.yaml |
arm64 Jetson, same Dockerfile (ros:humble is multi-arch) |
jetson-native |
package_manifest.jetson-native.yaml |
arm64 Jetson with host ROS2 — no docker |
- Docker targets (
x86-docker,jetson-docker) build therobonix-nav2image fromdocker/Dockerfile(ROS2 Humble +navigation2+nav2-bringup+pointcloud-to-laserscan).scripts/start.shruns it with--network host --ipc=host, bind-mounting the package androbonix-api. Use this where the host has no ROS2. - Native target (
jetson-native) requiresros-humble-nav2-bringup+ros-humble-navigation2on the host:
bash
sudo apt install ros-humble-nav2-bringup ros-humble-navigation2 \
ros-humble-pointcloud-to-laserscan
scripts/build.sh verifies the host packages; scripts/start_native.sh
sources the host ROS2 and runs the bridge directly. The dispatch honours
ROBONIX_NAV2_FORCE / ROBONIX_NAV2_PLATFORM overrides.
To add a target, drop a new package_manifest.<target>.yaml whose build: /
start: lines set RBNX_BUILD_TARGET and run the right scripts; the dispatch in
build.sh / start.sh already keys off that variable.
Config (passed via Driver(CMD_INIT, config_json))
- name: nav2
url: https://github.com/syswonder/service-navigation-rbnx
branch: main
config:
params_profile: sim # sim | slam | default → config/nav2_params_<profile>.yml
params_file: "" # absolute / package-relative path to override entirely
use_sim_time: true # true under a simulator that publishes /clock
action_wait_s: 90.0 # how long to wait for navigate_to_pose to advertise
topic_remap: {} # per-key override of atlas-resolved bindings
params_profile selects config/nav2_params_<profile>.yml. params_file
(absolute, or relative to the package root) overrides it entirely — useful when
an operator's tuning lives outside the package.
Params profiles
| Profile | Global costmap frame | Localization | Use when |
|---|---|---|---|
sim |
map (static layer) |
online SLAM (rtabmap) | Webots / any body with a SLAM provider publishing /map + map→odom, mapping while navigating |
slam |
map |
AMCL + map | Real deploy with a SLAM map→odom TF + static map |
default |
map |
AMCL + map | Generic map-based navigation |
Atlas contract dependencies
The wrapper resolves every topic it consumes through atlas
(ATLAS.find_capability + connect_capability) — it does NOT know which package
provides them on a given deploy. Each resolved topic is passed to nav2_bringup
as a launch remap.
Required (on_init returns Deferred until both are on atlas):
| Contract | Resolved into nav2 as |
|---|---|
robonix/service/map/occupancy_grid |
/map |
robonix/primitive/chassis/odom |
/odom |
Optional (init proceeds if absent; that observation source is just off):
| Contract | Resolved into nav2 as |
|---|---|
robonix/primitive/lidar/lidar |
/scan |
robonix/primitive/lidar/lidar3d |
/scanner/cloud |
Override any binding via the manifest's topic_remap block (e.g. to pin one of
several providers, or tap a downstream filter):
config:
topic_remap:
map: /robonix/map/occupancy_grid
scan: /scanner_normalized
TF prerequisites
Nav2 needs a healthy odom → base_link → sensor TF chain plus map → odom for
all three profiles (sim included — it navigates in the map frame). TF is still
a global ROS side-channel, not an atlas contract. The convention: the SLAM
provider owns map → odom, the chassis provider owns odom → base_link, and a
body-description provider owns base_link → sensor_* via robot_state_publisher.
Under sim, an online-SLAM provider (rtabmap in the Webots example) supplies
both map → odom and the /map the costmaps' static layer consumes.
If your costmap references /scan but only primitive/lidar/lidar3d
(PointCloud2) is on atlas, the image ships pointcloud_to_laserscan — add it to
the launch, or switch the costmap layer to VoxelLayer with
data_type: PointCloud2.
DDS / sysctl tuning (multi-container deploys)
The docker targets run Nav2 with --network host and a UDP-only FastDDS profile
(docker/no_shm_profile.xml) — SHM is disabled because cross-container SHM
locks are unreliable, and the transport stays on all interfaces so the sim's
topics remain discoverable.
On a host with many DDS participants (a simulator brings up dozens), the default
net.core.rmem_max (208 KB) can overflow under the endpoint-discovery burst and
drop Nav2's lifecycle change_state replies, stalling bring-up
("failed to send response … timeout"). If you see Nav2 nodes stuck
unconfigured / inactive, raise the host UDP buffers — the same fix used for
high-rate lidar:
sudo sysctl -w net.core.rmem_max=2147483647
sudo sysctl -w net.core.wmem_max=2147483647
# persist: echo 'net.core.rmem_max=2147483647' | sudo tee /etc/sysctl.d/10-dds.conf
On a robot with a quiet network and tuned buffers this is unnecessary.
Layout
nav2_wrapper_rbnx/
├── package_manifest.yaml x86-docker (default)
├── package_manifest.jetson-docker.yaml
├── package_manifest.jetson-native.yaml
├── nav2_wrapper/
│ └── atlas_bridge.py Service: lifecycle + navigate/status/cancel gRPC
├── scripts/
│ ├── build.sh codegen + per-target build (RBNX_BUILD_TARGET)
│ ├── start.sh docker ↔ native dispatch
│ └── start_native.sh host ROS2 path
├── docker/
│ ├── Dockerfile ROS2 Humble + Nav2 + pointcloud_to_laserscan
│ ├── entrypoint.sh
│ └── no_shm_profile.xml UDP-only FastDDS profile
└── config/ vendored Nav2 params
├── nav2_params.yml
├── nav2_params_slam.yml
└── nav2_params_sim.yml
License
This package: Apache-2.0 (matches Nav2 upstream).