isaac-slam service is SkyCore’s GPS-denied navigation brain. It runs NVIDIA Isaac ROS Visual SLAM on an Intel RealSense stereo-infrared + IMU camera, converts the visual-inertial odometry into a MAVLink vision pose, and feeds that pose directly into the ArduPilot EKF so the vehicle can hold position and navigate without GPS — indoors, in tunnels, under bridges, or anywhere the satellites drop out. The same service also builds a 3D nvblox map that can be saved, reloaded, and turned into a live obstacle-distance feed.
This is an opt-in, GPU-bound service (Compose profile slam, runtime: nvidia, network_mode: host) that is not part of the default COMPOSE_PROFILES. It runs on the Jetson alongside core, mavproxy, and ws_proxy.
isaac-slam is the one on-drone service that does not talk to the flight controller through the mavp2p UDP fabric. It reads vehicle state (/mavros/state) over ROS 2 DDS and writes vision pose to the FC over a direct serial MAVLink link. See the serial-port caveat below.The end-to-end pipeline
| Stage | Component | File |
|---|---|---|
| Sensor | RealSense D435, dual mono infra + gyro/accel fused | configs/launch/isaac_ros_visual_slam_realsense.launch.py |
| VIO | isaac_ros_visual_slam VisualSlamNode (IMU fusion on) | same launch file |
| Mapping | nvblox_node (static occupancy) | services/navigation/slam_service.py:400 |
| Bridge | pose_bridge_with_covariance ROS 2 node | services/navigation/pose_bridge_with_covariance.py |
| Obstacles | ObstacleProcessor decorator (opt-in) | services/navigation/nvblox_obstacle_bridge.py |
| Orchestration | SlamService → subprocess launches | services/navigation/slam_service.py |
docker/issac-slam/isaac_ros-dev/src/isaac_ros_common/docker/ — note the directory is spelled issac-slam on disk while the Compose service and image use isaac.)
RealSense + Visual SLAM configuration
The camera is brought up with both infrared streams (color and depth disabled) plus the IMU, and the Visual SLAM node is configured for stereo + IMU fusion:configs/launch/isaac_ros_visual_slam_realsense.launch.py
/visual_slam/tracking/vo_pose (a geometry_msgs/PoseStamped in an ENU frame), which the pose bridge subscribes to. SlamService also watches /visual_slam/status and /odom as liveness signals and republishes a /diagnostics heartbeat.
The pose bridge
pose_bridge_with_covariance.py is the heart of GPS-denied nav. On each incoming SLAM pose it:
- Transforms ENU → NED (
PoseTransformer.transform_enu_to_ned). - Estimates velocity, classifies a movement mode, and computes adaptive covariance.
- Rate-limits to
send_rate_hz(default 30 Hz) and sends aVISION_POSITION_ESTIMATEover the direct MAVLink serial link.
HEARTBEAT (as MAV_COMP_ID_VISUAL_INERTIAL_ODOMETRY), a 0.1 Hz SYSTEM_TIME clock sync, a one-shot EKF-origin set 10 s after the first pose, and periodic VISO alignment.
The ENU → NED transform
This is orientation-critical — every consumer of the vision pose depends on it, so a future refactor must reproduce it exactly (pose_bridge_with_covariance.py:186):
| Axis | ENU (Isaac output) | NED (sent to FC) |
|---|---|---|
| Position X | East | y (North = ENU Y) |
| Position Y | North | x (East = ENU X) |
| Position Z | Up | -z (Down = −ENU Z) |
| Roll | roll_enu | roll_enu (unchanged) |
| Pitch | pitch_enu | -pitch_enu (inverted) |
| Yaw | yaw_enu | -yaw_enu + π/2 |
Adaptive covariance
Covariance is scaled up when the vehicle moves fast or tracking confidence is low, so the EKF trusts the vision pose less during aggressive manual movement and more when holding still (calculate_adaptive_covariance):
- Movement-mode scale:
STATIONARY×1.0,SLOW_MANUAL×2.0,FAST_MANUAL×5.0,AUTONOMOUS×1.5. Effective thresholds (declared ROS-param defaults, sinceSlamServicepasses neither):movement_threshold0.1 m/s,fast_movement_threshold1.0 m/s. The dataclass carries lower 0.05/0.5 fallbacks, but those are shadowed and never used at runtime. - Confidence scale:
4 − tracker_confidence(default confidence 3 → factor 1). - Base variances at launch are
position_variance=0.5,orientation_variance=1.0(passed bySlamService, overriding the node’s own 0.15/0.5 defaults). Results are clamped to pos[0.01, 1.0], ori[0.05, 2.0]. - Anomaly guard: a position jump > 2.0 m or velocity > 5.0 m/s forces high covariance (pos 1.0, ori 2.0) for that frame.
The computed covariance matrix is not carried by the transmitted message.
pose_callback sends the basic vision_position_estimate_send(...), which has no covariance field — the adaptive matrix is used for logging and mode tracking. A send_vision_position_delta path (which does support covariance) exists as a fallback but is not the active send. Preserve this distinction if you touch the send path.EKF origin & VISO alignment caveats
Because there is no GPS, the EKF has no absolute reference — so the bridge fabricates one. Ten seconds after the first pose (and oncefirst_pose_received), it sends SET_GPS_GLOBAL_ORIGIN followed by MAV_CMD_DO_SET_HOME using a hardcoded default origin:
pose_bridge_with_covariance.py:118
RC7_OPTION=80 set in the ArduPilot parameters. It fires immediately on the first pose, again after viso_align_delay (2 s), and periodically every 30 s while the vehicle is stationary.
The serial-port caveat
The bridge opens a direct pyMAVLink serial connection — it does not go throughmavp2p. It walks a candidate list at 921600 baud and connects to the first that answers a heartbeat:
pose_bridge_with_covariance.py:132
nvblox mapping: mapping vs patrol
nvblox builds the 3D occupancy map. SlamService picks its config from MAPPING_ENABLED — the launch mode is always static (static occupancy), only the params file changes (slam_service.py:400):
Mapping mode
MAPPING_ENABLED=true → /home/configs/nvblox_mapping.yml. Builds and saves new high-quality maps. Map loading is skipped in this mode.Patrol mode
MAPPING_ENABLED=false → /home/configs/nvblox_patrol.yml. Loads a pre-built map for obstacle avoidance during autonomous runs.Loading and saving maps
In patrol mode, ifNVBLOX_MAP_PATH is set and the file exists, SlamService._load_map() calls the nvblox service after startup:
{ip}:map_loaded on Redis. There is also a nvblox_map_loader node exposing nvblox_map_loader/save_map for programmatic saves.
Obstacle avoidance (opt-in)
WhenOBSTACLE_AVOIDANCE=true, the pose bridge imports nvblox_obstacle_bridge and applies add_obstacle_functionality, a class decorator that grafts obstacle handling onto PoseBridgeWithCovariance at import time. The obstacle processor (nvblox_obstacle_bridge.py):
- Subscribes to
/nvblox_node/static_occupancy_grid(nav_msgs/OccupancyGrid). - Buckets occupied cells (occupancy ≥
obstacle_threshold70) into 72 angular sectors (5° each), keeping the nearest obstacle per sector, with exponential smoothing and a 3-reading minimum before reporting. - Sends
OBSTACLE_DISTANCEMAVLink at 10 Hz, distances in cm,min0.5 m /max20 m, in theMAV_FRAME_BODY_FRD(frame 12) body frame, so ArduPilot’s proximity/avoidance layer can act on it.
Obstacle avoidance rides on the same MAVLink connection as the pose bridge (it reuses
self.mavlink). It only activates if the import succeeds; otherwise the bridge logs “Running WITHOUT obstacle avoidance functionality” and continues with pose-only injection.Service orchestration & health
main.py builds a ServiceContainer (dependency injection) and registers services from IsaacConfig toggles. SlamService.start() runs a strict sequence:
Init ROS + announce
Create the
slam_service ROS node, subscribe to /visual_slam/status, /visual_slam/tracking/vo_pose, /odom; optionally TTS-announce “slam_starting”.Launch nvblox
pkill -f nvblox_node, then subprocess.Popen the nvblox_examples_bringup realsense_example.launch.py with the mapping/patrol params file. Wait ~10 s and verify nvblox_node is in ros2 node list.Load map
In patrol mode, call
/nvblox_node/load_map if NVBLOX_MAP_PATH is set. Failure is non-fatal (continues without a pre-loaded map).Launch pose bridge
subprocess.Popen pose_bridge_with_covariance.py with --ros-args params (serial port, baud, coordinate system, covariance, EKF origin, OBSTACLE_AVOIDANCE passed through the environment).subprocess.Popen children, not ROS 2 lifecycle nodes — a crash of main.py orphans/relaunches them. ROS_DOMAIN_ID=1 is exported in each subprocess launch string; RMW_IMPLEMENTATION=rmw_fastrtps_cpp is exported only in the nvblox launch string (slam_service.py:437), while the pose-bridge launch string exports OBSTACLE_AVOIDANCE instead (slam_service.py:536-537). Navigation-mode changes are surfaced on Redis:
| Redis channel | Publisher | Meaning |
|---|---|---|
{ip}:slam_status | SlamService | stopped only (slam_service.py:241); start status goes to the separate slam:status channel as a get_status() dict |
{ip}:navigation_mode | SlamService | {mode: SLAM|GPS, status: enabled} |
{ip}:map_loaded | SlamService | map load success + path |
{ip}:navigation_error | SlamService | SLAM/bridge not ready |
{ip}:mode_change | MavrosStateMonitor | ArduPilot flight-mode / armed transitions |
Configuration reference
Sourced from.env.example (§ ISAAC SLAM SERVICE) and the Compose isaac-slam service:
| Env var | Default | Purpose |
|---|---|---|
SLAM_ENABLED | true | Register/start the SLAM service at all |
MAPPING_ENABLED | false | Mapping (nvblox_mapping.yml, save) vs patrol (nvblox_patrol.yml, load) |
NVBLOX_MODE | static | nvblox occupancy mode |
NVBLOX_MAP_PATH | /home/maps/map.nvblx | Map to load in patrol mode (skipped if unset) |
OBSTACLE_AVOIDANCE | false | Enable the OBSTACLE_DISTANCE bridge (decorator) |
FRONT_CAMERA_SERIAL | <aws-account-id> | RealSense serial for SLAM stereo |
BACK_CAMERA_SERIAL | <aws-account-id> | Optional second camera (enables dual-camera mode) |
DOCKING_CAMERA_SERIAL | “ (empty) | RealSense for rover docking (separate feature) |
ROS_DOMAIN_ID | 1 | DDS domain shared with core/MAVROS (load-bearing) |
SLAM_AUTO_START / SLAM_DEFAULT_MODE | true / SLAM | Declared in .env.example; not consumed by the observed start path (SLAM starts whenever SLAM_ENABLED=true) — treat as advisory |
NVIDIA_VISIBLE_DEVICES / NVIDIA_DRIVER_CAPABILITIES | all / all | GPU exposure (runtime: nvidia) |
OPENAI_API_KEY | — | TTS voice announcements (audio service) |
serial_port, baudrate, send_rate_hz, position_variance, orientation_variance, ekf_latitude/longitude/altitude, VISO/covariance knobs) are passed as ROS 2 params by SlamService._launch_pose_bridge, not as top-level env vars — change them there or via --ros-args -p.
Running it
Related pages
Microservices & Container Profiles
Where
isaac-slam sits in the Compose topology, GPU/audio device wiring, and boot ordering.MAVLink Routing (mavp2p)
Why SLAM bypasses the UDP hub and injects vision pose over a direct serial link.
Detection, ArUco Landing & Docking
The RealSense-D435 ArUco rover docking state machine that also lives in
isaac-slam.RTK NTRIP GPS Corrections
The complementary path: cm-level GPS when satellites are available.
Redis Message Bus
The
{ip}: channel conventions and the MY_IP vs wg0 namespacing gotcha.Guided Velocity Control & Safety
How GUIDED-mode setpoints ride on top of a SLAM-fed EKF.

