Skip to content

Manual integration reference

For guided setup, start with Integrate your robot. This reference explains the profile, simulator and runtime contracts in detail.

This is the end-to-end path for a differential-drive robot with a 2D lidar. You bring the robot description and ROS interfaces; DWARL builds the training model, generates data, trains a policy, evaluates checkpoints and exports an ONNX bundle for its Nav2 controller plugin.

Your URDF + dwarl_robot.yaml
           ↓
     train → test → deploy
                       ↓
           policy.onnx + deploy.yaml
                       ↓
Nav2 global path → DWARL controller → velocity commands → your base

DWARL replaces the local controller, not the complete navigation system. There is no pretrained navigation-quality policy included.

1. Prepare your workspace

Use pixi: it installs Python 3.12, ROS 2 Jazzy and Gazebo Harmonic, so no system ROS install is needed (Linux x86-64).

git clone https://github.com/nobleo/dwarl.git
cd dwarl
pixi install
pixi run dwarl doctor --sim mujoco

URDF/xacro imports use pixi's xacro; referenced description/mesh packages must be in your robot workspace. Build the native ROS packages before Gazebo testing or Nav2 deployment.

For the rest of this guide, set these to your actual paths:

export DWARL_REPO="$HOME/repos/dwarl"
export DWARL_ROBOT_WS="$HOME/repos/my_robot"
export INTEGRATION="$DWARL_ROBOT_WS/src/my_robot_dwarl"

Then open a DWARL shell. It activates ROS, your robot workspace (once set up with pixi run setup, below) and DWARL's ROS packages; nothing needs to be sourced:

cd "$DWARL_REPO"
pixi shell

Keep this working directory for DWARL commands: runs/ and .dwarl/ are stored relative to it. $DWARL_ROBOT_WS/src is added to DWARL_ROBOT_PATH automatically, so description packages are found from source for model loading; Gazebo launch packages are built by pixi run setup.

What must already work on your robot?

You provide Used for
URDF/xacro with wheel joints, inertials and lidar link MuJoCo training model
Actual footprint, velocity/acceleration limits and lidar settings Training/runtime contract
sensor_msgs/msg/LaserScan and nav_msgs/msg/Odometry Policy input and navigation
TF: map → odom → base_link → lidar (use your base/lidar names) Path and scan transforms
Base driver accepting Twist or TwistStamped Executing velocity commands
Map server + localization, or SLAM supplying a map and localization TF Real-robot Nav2 navigation
A robot-only Gazebo launch adapter Managed Gazebo tests, if desired

You can train and test in MuJoCo before implementing the Gazebo adapter. If you want the same run to support Gazebo testing, prepare the adapter in section 6 and set ros.sim_launch before starting that run.

2. Add a robot integration

Keep the integration in your robot repository, not inside DWARL. A small ROS package is convenient when you also need launch files:

mkdir -p "$DWARL_ROBOT_WS/src"
cd "$DWARL_ROBOT_WS/src"
ros2 pkg create my_robot_dwarl --build-type ament_cmake --license MIT
mkdir -p "$INTEGRATION/launch"
cd "$DWARL_REPO"

Use your project's license when creating its package. You can instead use an existing robot package. The intended layout is:

my_robot_dwarl/
├── package.xml
├── CMakeLists.txt
├── dwarl_robot.yaml
├── launch/
│   └── simulation.launch.py     # your adapter, needed for Gazebo tests
├── policies/                    # created by dwarl deploy
└── selected_policy.yaml         # created by dwarl deploy

In CMakeLists.txt, add these before ament_package():

install(FILES dwarl_robot.yaml DESTINATION share/${PROJECT_NAME})
install(DIRECTORY launch DESTINATION share/${PROJECT_NAME})

Declare runtime dependencies in package.xml for your launch files (typically launch, launch_ros, ament_index_python, python3-yaml, dwarl_bringup and your robot's description/simulation packages). Policies will be addressed by their source path or copied to the target robot; they do not need to be baked into the build.

Create dwarl_robot.yaml

Download the profile template, copy it into your integration and replace its example values. It is a schema example, not a model of your robot.

cp "$DWARL_REPO/src/dwarl/templates/dwarl_robot.yaml" "$INTEGRATION/dwarl_robot.yaml"

The profile describes:

Section What to set
description URDF/xacro URI, base link and xacro arguments
drive Left/right wheel joints and MuJoCo actuator parameters
limits Linear/angular speed and acceleration limits in SI units
footprint Ordered, strictly convex polygon in the base frame, or radius
lidar Lidar link and post-filter field of view, range and ray count
collision, physics Training contact geometry and dynamics
ros Actual frames, topics, command type and optional simulation adapter
deployment Where bundles and the selected-policy file are installed

Paths may be relative to the profile, absolute, or package://package/path. Use description.urdf for a URDF or xacro. MJCF profiles are also supported, but must satisfy DWARL's internal model naming contract; URDF is the normal integration route.

Model conventions: x points forward, y left, z up. Drive wheels must be aligned laterally, with the left wheel at positive y and both joint axes pointing along base +y. Wheel radius comes from cylindrical/spherical collision geometry; set drive.wheel_radius if that geometry is absent. Supply sensible inertials.

DWARL simplifies collisions: drive wheels retain their geometry; the chassis becomes a footprint prism. Casters may need collision.supports spheres or explicit keep_collision_links; set these from your model so it rests correctly. chassis_z and support positions are in the base frame, not world coordinates. chassis_mass adds mass, so do not duplicate mass already represented by the URDF.

Lidar mounting is read from the URDF and real scans use live TF. Configure the scan after any filtering, including its useful field of view. The controller reprojects scans into the trained ray layout; it does not make unseen regions visible. Match the robot's real sensing and command limits before training.

For the standard bringup, keep ros.map_frame: map: some Nav2 global-frame settings currently remain fixed to map. Other global-frame names need a customized Nav2 configuration.

Validate the model

dwarl robot check "$INTEGRATION"
dwarl robot view "$INTEGRATION"
dwarl train "$INTEGRATION" --print-config
dwarl train "$INTEGRATION" --dry-run

Check reported mass, wheel radius/separation, lidar pose and spawn height. The viewer shows the simplified training geometry on a floor. It should remain supported, rather than fall through the floor or tip because of missing supports. Close the viewer to continue. This checks the model, not navigation performance.

3. Smoke-test the training pipeline

dwarl train "$INTEGRATION" --name robot-smoke \
  --num-envs 2 --max-iterations 2 \
  --set train.save_interval=1 \
  --set train.validation_interval=1 --set train.validation_cases=2 \
  --set train.data.train_maps=1 --set train.data.validation_maps=1 \
  --set train.data.test_maps=1 --set train.data.pairs_per_map=3
dwarl runs list

Copy the printed run directory, then exercise resume and inference:

SMOKE="runs/<your-smoke-run>"
dwarl train --resume "$SMOKE" --max-iterations 4
dwarl test --run "$SMOKE" --case doorway --sim mujoco --show --set test.max_cases=1

The resume budget is four total iterations, not four additional iterations. The smoke policy checks data, checkpointing and export/inference; navigation failures are expected. Test automatically exports the selected checkpoint to ONNX.

On the first run, DWARL freezes the robot assets and generates maps, spawns and A* paths before training begins. Progress messages show this work. Normal defaults use 128/16/16 train/validation/test maps and 32 pairs per map. Matching runs reuse .dwarl/datasets/; changes to robot geometry, seed or data settings can regenerate it. Set --set train.data.workers=4 to parallelize generation if resources allow.

4. Train and monitor

dwarl train "$INTEGRATION" --name navigation --num-envs 64 --max-iterations 3000 \
  --set train.validation_interval=200

Reduce --num-envs for a smaller machine. Device selection is automatic, with --device cpu and --device cuda overrides. Add --show for a viewer or --detached for background training. The training budget is a starting point, not a guarantee of navigation quality.

The command above explicitly enables intermediate validation so that the following --checkpoint best examples have selection evidence. Intermediate checks and early stopping are otherwise disabled by default. Optionally add --set train.benchmark_interval=500 for canonical checks or --set train.early_stop.enabled=true for validation-based plateau stopping. See check scheduling and patience.

RUN="runs/<your-navigation-run>"

In a second activated terminal, monitor training:

tensorboard --logdir runs --host localhost --port 6006

Open http://localhost:6006. Events appear after data generation when training begins. For one run, use --logdir runs/<run>/checkpoints. Reward, episode length, losses and throughput help diagnose learning; scored validation establishes how the policy navigates. See monitoring and resume.

5. Evaluate a trained checkpoint

latest is the latest completed checkpoint; best requires completed periodic validation. Use an iteration number to pin a checkpoint if training is still running and you want stable comparisons.

dwarl test --run "$RUN" --checkpoint best --dry-run
dwarl test --run "$RUN" --checkpoint best --suite benchmark --sim mujoco
dwarl test --run "$RUN" --checkpoint best --suite validation --sim mujoco
dwarl test --run "$RUN" --checkpoint best --suite test --sim mujoco

The canonical benchmark includes open_space, corridor, narrow_corridor, doorway, l_turn, s_turn, u_trap, dead_end, tight_gap and clutter. Use --case doorway --show for one scenario, --repeats 3 for repeated execution, or omit --case for the full suite. Choose checkpoints using validation, not the held-out test split.

Read runs/<run>/tests/<test-id>/report/README.md and summary.json. Exit 1 means completed navigation failed; exit 2 means an infrastructure/input failure. Ctrl+C preserves partial results. Results explains the metrics.

6. Add Gazebo testing

Skip this section if you only need MuJoCo evaluation and hardware deployment. DWARL can orchestrate your simulator, but cannot invent a Gazebo model, drive plugin or sensor bridge from the training profile.

Create launch/simulation.launch.py in your integration and set:

ros:
  # Keep the other ros settings from your profile.
  sim_launch: {package: my_robot_dwarl, file: simulation.launch.py}

The adapter must accept these launch arguments:

Argument Required behavior
world Start Gazebo using the supplied SDF, rather than a fixed robot world
x, y, yaw Spawn the robot at the requested world pose
gui true shows the GUI; false runs headless

Reuse your existing simulation launch and map these arguments to its own names. It must start only the simulator and robot interfaces, including robot state publisher, sensors, drive controller and bridges. DWARL starts map server, localization transform and Nav2. Do not start a second navigation stack.

The adapter contract is:

  • Publish /clock, the profile's scan and odometry topics, and odom → base TF.
  • Publish base-to-sensor TF and accept the configured command topic/message type.
  • Spawn the Gazebo entity as robot. Managed scoring currently uses that name and /world/default/dynamic_pose/info; generated test worlds are named default.
  • Use odometry starting at zero at spawn. The benchmark applies the requested spawn pose as map → odom; world-coordinate odometry would apply it twice.
  • Preserve the inherited ROS domain and Gazebo partition so managed cases remain isolated.

The included TurtleBot simulator adapter shows how to include Gazebo and a robot spawn launch. Adapt that pattern to your model and bridges; it is not a substitute for your robot's simulator. For a base launch that already accepts the five arguments above, the wrapper can be as small as:

from pathlib import Path
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration


def generate_launch_description():
    # Replace these names with your existing robot-only simulator launch.
    source = Path(get_package_share_directory("my_robot_simulation")) / "launch/robot.launch.py"
    defaults = {"world": "", "x": "0", "y": "0", "yaw": "0", "gui": "false"}
    return LaunchDescription([
        *(DeclareLaunchArgument(key, default_value=value) for key, value in defaults.items()),
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource(str(source)),
            launch_arguments={key: LaunchConfiguration(key) for key in defaults}.items(),
        ),
    ])

If your simulator calls them x_pose, world_file, etc., change the argument keys passed to that included launch. Its implementation must still satisfy the entity, odometry, bridge and TF contract above.

Add the adapter to the profile before training if you plan to use Gazebo: the run freezes its ROS settings too. If you add or change this interface after training, start a new run rather than editing the saved contract.

Build the integration after adding its launch file (from $DWARL_REPO, in a new terminal):

pixi run setup              # select my_robot_dwarl; builds it and its dependencies, then checks Gazebo
pixi shell

Launch-file and config edits take effect through the symlink install. Gazebo tests and deployment with a launch adapter prepare the required ROS packages incrementally, both through pixi task shortcuts and direct CLI commands inside pixi shell. You can also rebuild explicitly with pixi run build-robot.

Then test:

dwarl doctor --sim gazebo
dwarl test --run "$SMOKE" --case doorway --sim gazebo --show --set test.max_cases=1
dwarl test --run "$RUN" --checkpoint best --suite benchmark --sim both
dwarl test --run "$RUN" --checkpoint best --suite test --sim both

If the smoke run predates the adapter, create a new smoke run first. --sim both uses the same case manifest for both backends. Gazebo starts a fresh stack per case and takes longer. Its spawn-transform localization tests the controller, not AMCL. Ground-truth scoring requires Gazebo Transport 13 / Messages 10 Python bindings; the generic ROS Pose_V bridge loses model names.

7. Export and install the network

There is no separate dwarl export command. Test and deploy automatically export an exact checkpoint into an immutable ONNX bundle. Deploy also installs it in your integration:

dwarl deploy "$INTEGRATION" --run "$RUN" --checkpoint best --dry-run
dwarl deploy "$INTEGRATION" --run "$RUN" --checkpoint best

Copy the installed bundle path printed by deployment:

export BUNDLE="$INTEGRATION/policies/<printed-bundle-id>"

The directory contains policy.onnx, deploy.yaml, the frozen profile/model and assets, resolved configuration and provenance hashes. deploy.yaml carries the observation layout, scan preprocessing/history, action mapping, control rate, limits and ROS settings needed to run that network correctly.

Keep the whole bundle together; copying only the ONNX file loses its runtime contract. Do not edit hashed bundle files. Export validates compatibility, not navigation quality: deployment does not require all recorded tests to have passed. Changes to robot geometry/profile may require a new training run; deployment checks the target against the frozen training model.

selected_policy.yaml records the installed bundle's relative path and checkpoint. The generic Nav2 launch takes model_dir explicitly; it does not automatically read that selection file. Your robot launch can read selected_policy.yaml and pass the resolved path to navigation.launch.py to automate selection. Loading new weights requires a Nav2 restart/reconfigure, not a ROS rebuild.

For that convenience wrapper, download this launch file into your integration's launch/ directory:

cp "$DWARL_REPO/src/dwarl/templates/navigation.launch.py" "$INTEGRATION/launch/navigation.launch.py"

Set deployment.launch: {package: my_robot_dwarl, file: navigation.launch.py} in the profile and rebuild the integration once. Then, after starting your robot interfaces and localization, use:

ros2 launch my_robot_dwarl navigation.launch.py integration:="$INTEGRATION"

This wrapper uses the selected-policy file from that directory and includes the same standard navigation launch described below. For a robot receiving only a copied bundle, use the explicit model_dir launch instead.

Transfer to the robot computer

Use the dedicated pixi runtime environment on supported Linux x86-64 targets. It builds and activates the native packages automatically. The C++ controller uses ONNX Runtime; it does not need the training Python environment, PyTorch, MuJoCo or a training GPU.

For example, replacing the login and destination:

ssh robot@robot-host 'mkdir -p ~/dwarl-policies'
scp -r "$BUNDLE" robot@robot-host:~/dwarl-policies/

On the robot, set BUNDLE to the copied directory's absolute path.

8. Run with Nav2

Use DWARL's navigation launch

First launch your robot drivers, state publisher, scan filtering, odometry, map server and localization (or your SLAM stack). They must provide the interfaces from section 1. Then, on that same computer:

ros2 launch dwarl_bringup navigation.launch.py \
  model_dir:="$BUNDLE" use_sim_time:=false

Use use_sim_time:=true for a simulator. The launch starts controller, planner, behavior and BT navigator servers, lifecycle management, and a collision monitor. It reads the bundle to set the robot footprint, scan topic, limits and control rate. It does not start drivers, localization or a map server. Avoid running it alongside another full Nav2 navigation launch.

The default command path is controller → cmd_vel_raw → collision monitor → your configured command topic. Match ros.cmd_vel_stamped to your base driver or mux. The launch also offers cmd_vel_topic:=... and cmd_vel_stamped:=true|false overrides for command routing.

Open RViz with your robot/map display and send a Nav2 Goal. Check an open route, turning and a passage, then inspect motion against the scored simulation results. The policy controls position/path following; it does not enforce a final goal heading. The provided goal checker therefore uses a relaxed yaw tolerance.

Already have your own Nav2 stack?

Keep that launch and load the plugin in its existing controller server instead. This snippet illustrates the replacement, not a full Nav2 parameter file:

controller_server:
  ros__parameters:
    controller_frequency: 10.0  # set to 1 / deploy.yaml control.dt
    controller_plugins: [FollowPath]
    FollowPath:
      plugin: dwarl_controller::DwarlController
      model_dir: /absolute/path/to/complete/bundle
      expected_robot_id: my_robot
      scan_topic: /scan
      scan_timeout: 0.5
      transform_tolerance: 0.2
      inference_threads: 1

Match the actual bundle's robot ID, frequency, footprint, frames, scan topic, goal tolerance and command limits. Retain your global planner, costmaps, localization, lifecycle setup and command routing. The supplied Nav2 configuration and parameter renderer show which values the standard launch derives from the bundle. The controller already applies the trained action rate limits; account for any additional velocity smoothing in your stack.

Troubleshooting the first integration

Symptom Check
Profile/package not found Pass the exact profile path; run pixi run setup (or set DWARL_ROBOT_PATH)
Multiple integrations found Pass the intended dwarl_robot.yaml, not the workspace root
Robot falls or tips in the viewer URDF inertials, wheel collision shapes and support/caster geometry
Startup is slow Generation progress; matching caches are reused; reduce smoke data or increase workers
best is unavailable Complete periodic validation, or use latest for an infrastructure test
Gazebo readiness timeout Case launch.log, clock, scan, odom, TF, entity name robot and installed adapter
Many nav2_aborted/timeouts, lifecycle "heartbeat" errors compute.json real_time_factor; leftover DWARL Gazebo and powersave warnings from dwarl doctor --sim gazebo; compare only runs made under the same load
Robot appears at the wrong map pose Odometry origin at spawn and duplicate map → odom publishers
Controller refuses/stops on scan input Scan age, frame TF, filtering and configured topic
Nav2 runs but the base does not move Output topic, Twist vs TwistStamped, mux routing and collision-monitor state
Bundle incompatible with target Profile or referenced geometry changed after training

For all CLI switches and resolved defaults, see the generated reference.