Skip to content

Migrating from robot_localization to FusionCore

If you're coming from robot_localization and want to switch, this guide walks you through it step by step: no prior knowledge of FusionCore required.


Three files, one command

Everything you need is already in this repo. Here's the big picture before we dive in:

File What it does
fusioncore_ros/config/fusioncore.yaml (or your hardware config) Tells FusionCore about your robot: IMU noise, GPS quality, sensor offsets
fusioncore_ros/config/nav2_params.yaml A ready-to-use Nav2 config pre-wired to FusionCore's output (if you use Nav2)
fusioncore_ros/launch/fusioncore_nav2.launch.py One launch file that starts both FusionCore and Nav2 together

Once you have your robot config set up, the entire stack starts with:

ros2 launch fusioncore_ros fusioncore_nav2.launch.py \
  fusioncore_config:=/path/to/your_robot.yaml

That's it. FusionCore starts, configures itself, activates, and Nav2 comes up 8 seconds later once localization is publishing. No separate navsat_transform_node, no feedback loop, no topic wiring.

Don't use Nav2? Use fusioncore.launch.py instead:

ros2 launch fusioncore_ros fusioncore.launch.py \
  fusioncore_config:=/path/to/your_robot.yaml

What's changing (and what isn't)

With robot_localization, you wired two nodes together:

/imu/data ──────────────────────────────────────────────────┐
/odom/wheels ──────→ ekf_node → /odometry/filtered          │
/odometry/filtered ─→ navsat_transform_node → GPS in ENU ───┘
/fix ──────────────→ navsat_transform_node

Two nodes, two config files, feedback loop between them, lots of parameter tuning.

With FusionCore, it's one node:

/imu/data ────┐
/odom/wheels ─┤→ fusioncore_node → /fusion/odom
/gnss/fix ────┘                  → TF: odom → base_link

GPS fusion happens inside: no navsat_transform_node, no feedback loop. The output topic changes from /odometry/filtered to /fusion/odom.

What stays the same: - Your robot still publishes /imu/data, /odom/wheels, /gnss/fix (you may need a remap: see Step 4) - Nav2 still works the same way, it just reads from /fusion/odom instead of /odometry/filtered - The odom → base_link TF is still published the same way


Step 1: Remove robot_localization from your launch

In your launch file, remove or comment out: - The ekf_node launch - The navsat_transform_node launch - Any parameter files you were loading for those nodes

You don't need to replace them with anything complex. Either use the provided launch file (recommended):

ros2 launch fusioncore_ros fusioncore_nav2.launch.py \
  fusioncore_config:=/path/to/your_robot.yaml

Or if you have your own launch file, add FusionCore as a lifecycle node:

from launch_ros.actions import LifecycleNode

fusioncore_node = LifecycleNode(
    package="fusioncore_ros",
    executable="fusioncore_node",
    name="fusioncore",
    namespace="",
    output="screen",
    parameters=["/path/to/fusioncore.yaml"],
)

FusionCore is a lifecycle node, so you need to configure and activate it after launch. The provided fusioncore_nav2.launch.py does this automatically (2-second delay, then configure → activate). If you're writing your own launch file, see the launch file source at fusioncore_ros/launch/fusioncore_nav2.launch.py for how to wire the lifecycle transitions.


Step 2: Create your robot config

FusionCore needs one YAML file that describes your hardware. You can start from a ready-made config for your platform:

Or start from scratch with this minimal template:

fusioncore:
  ros__parameters:
    base_frame: base_link    # must match your robot's base TF frame
    odom_frame: odom
    publish_rate: 100.0
    publish.force_2d: true   # set true for ground robots

    imu.has_magnetometer: false
    imu.gyro_noise: 0.005    # rad/s: check your IMU datasheet
    imu.accel_noise: 0.1     # m/s²
    imu.remove_gravitational_acceleration: false  # see gravity note below

    encoder.vel_noise: 0.05  # m/s
    encoder.yaw_noise: 0.02  # rad/s

    gnss.base_noise_xy: 2.5  # m: standard GPS. Use 0.5 for RTK float, 0.015 for RTK fixed
    gnss.base_noise_z: 5.0
    gnss.max_hdop: 4.0
    gnss.min_satellites: 4
    gnss.min_fix_type: 1     # 1=GPS, 2=DGPS/RTK_FIXED, 4=RTK_FIXED only
                             # WARNING: sensor_msgs/NavSatFix has no RTK_FLOAT status.
                             # Setting min_fix_type: 3 will silently starve the filter. Use 2 or 4.

    outlier_rejection: true
    outlier_threshold_gnss: 16.27
    outlier_threshold_imu: 15.09
    outlier_threshold_enc: 11.34
    outlier_threshold_hdg: 10.83

    adaptive.imu: true
    adaptive.encoder: true
    adaptive.gnss: true
    adaptive.window: 50
    adaptive.alpha: 0.01

    ukf.q_position: 0.01
    ukf.q_orientation: 1.0e-9
    ukf.q_velocity: 0.1
    ukf.q_angular_vel: 0.1
    ukf.q_acceleration: 1.0
    ukf.q_gyro_bias: 1.0e-5
    ukf.q_accel_bias: 1.0e-5

    input.gnss_crs: "EPSG:4326"
    output.crs: "EPSG:4978"
    output.convert_to_enu_at_reference: true
    reference.use_first_fix: true   # map origin = first GPS fix

GPS quality presets: if you want to override GPS noise thresholds for your environment without editing the robot config, you can layer an environment preset on top:

ros2 launch fusioncore_ros fusioncore_nav2.launch.py \
  fusioncore_config:=your_robot.yaml \
  env_config:=$(ros2 pkg prefix fusioncore_ros)/share/fusioncore_ros/config/env_urban.yaml

Available presets: env_open.yaml, env_urban.yaml, env_canopy.yaml.

Gravity removal: one thing to get right

There's one parameter where the naming is confusing:

# robot_localization:
imu0_remove_gravitational_acceleration: true
# means: "remove gravity before I fuse it"

# FusionCore:
imu.remove_gravitational_acceleration: true
# means: "my driver already removed gravity, add it back"

The names look the same and they mean opposite things, so the value must be inverted when you migrate.

IMU reads ~9.8 m/s² in z at rest (gravity present, the common case)
    robot_localization:  imu0_remove_gravitational_acceleration: true
    FusionCore:          imu.remove_gravitational_acceleration: false

IMU reads ~0.0 m/s² in z at rest (driver already removed it)
    robot_localization:  imu0_remove_gravitational_acceleration: false
    FusionCore:          imu.remove_gravitational_acceleration: true

robot_localization's flag means "the data has gravity in it, please take it out". FusionCore's means "the driver already took it out, please put it back", because the filter's measurement model expects specific force. Copying the value across unchanged makes FusionCore add a second 9.8 m/s² to data that already carries it, and a constant acceleration error double-integrates into position.

The reliable check is the sensor, not the old config. Run ros2 topic echo /imu/data --field linear_acceleration.z --once with the robot at rest. ~9.8 means false, ~0.0 means true. tools/rl_to_fusioncore.py performs this inversion for you and says so in its output.


Step 3: Nav2 setup (if you use Nav2)

This is the most common pain point when migrating: all the places in Nav2 that referenced /odometry/filtered need to point to /fusion/odom.

The easy path: use the bundled nav2_params.yaml at fusioncore_ros/config/nav2_params.yaml. It comes pre-wired to /fusion/odom and is configured for outdoor GPS navigation (differential drive, Regulated Pure Pursuit, no AMCL). The fusioncore_nav2.launch.py launch file uses it by default.

# This already uses the bundled nav2_params.yaml: nothing else needed
ros2 launch fusioncore_ros fusioncore_nav2.launch.py \
  fusioncore_config:=your_robot.yaml

Customizing: if you have your own nav2_params.yaml, pass it as an argument:

ros2 launch fusioncore_ros fusioncore_nav2.launch.py \
  fusioncore_config:=your_robot.yaml \
  nav2_params:=/path/to/your_nav2_params.yaml

Updating your existing Nav2 config manually: if you want to keep your own nav2_params.yaml, find and replace every instance of /odometry/filtered with /fusion/odom. The three places that need it:

bt_navigator:
  ros__parameters:
    odom_topic: /fusion/odom        # was /odometry/filtered

velocity_smoother:
  ros__parameters:
    odom_topic: /fusion/odom        # was /odometry/filtered

# if you had AMCL and are doing GPS-only outdoor navigation, remove it

GPS-only outdoor navigation (no map): remove AMCL. It has nothing to localize against without a static map, and the Nav2 global_frame must be set to odom everywhere: see Nav2 Integration.

Indoor navigation with a map: keep AMCL. FusionCore publishes odom → base_link. AMCL publishes map → odom. These are different TF edges and are fully compatible: there is no conflict.


Step 4: Remap your topics (if your robot uses non-default names)

FusionCore subscribes to these topics by default:

Input Parameter Default topic Your topic Change needed?
IMU imu.topic /imu/data probably the same usually no
Wheel odometry encoder.topic /odom/wheels often different likely yes
GPS fix gnss.fix_topic /gnss/fix often /fix often yes

The wheel odometry default is /odom/wheels, not the conventional /odom. That is deliberate: FusionCore publishes its own fused odometry, so defaulting to /odom would invite a feedback loop with its own output. Expect to set this one.

If your topics differ, set them in your config YAML:

fusioncore:
  ros__parameters:
    encoder.topic:  "/your/wheel/odom/topic"
    gnss.fix_topic: "/fix"

Clearpath Husky example (odom at /husky_velocity_controller/odom, GPS at /fix):

fusioncore:
  ros__parameters:
    encoder.topic:  "/husky_velocity_controller/odom"
    gnss.fix_topic: "/fix"

ROS 2 remaps also still work if you prefer them, and apply on top of whatever the parameter resolves to:

ros2 launch fusioncore_ros fusioncore_nav2.launch.py \
  fusioncore_config:=your_robot.yaml \
  --ros-args \
  -r /odom/wheels:=/husky_velocity_controller/odom \
  -r /gnss/fix:=/fix

Either way, check the startup log to confirm what it actually subscribed to. Pointing at a topic that does not exist fails silently: the filter still runs and publishes, it just never fuses that sensor.

[INFO] [fusioncore]: Encoder topic: /husky_velocity_controller/odom
[INFO] [fusioncore]: GNSS topic: /fix (sensor_msgs/NavSatFix)

FusionCore publishes to /fusion/odom. Any downstream node that was reading /odometry/filtered needs to be updated to read /fusion/odom: the bundled nav2_params.yaml already handles this for Nav2.


Full parameter mapping reference

EKF parameters

robot_localization (ekf_node) FusionCore Notes
frequency publish_rate
sensor_timeout automatic FC detects stale sensors per-sensor automatically
two_d_mode: true publish.force_2d: true
transform_time_offset automatic FC handles timing internally
transform_timeout automatic
predict_to_current_time always on FC always predicts to current time
smooth_lagged_data automatic FC replays buffered IMU for delayed GPS
history_length automatic FC uses a 1-second IMU ring buffer
imu0 (topic name) remap /imu/data FC subscribes to /imu/data by default
odom0 (topic name) encoder.topic defaults to /odom/wheels; set it or remap
imu0_config not needed FC fuses all available IMU axes automatically
odom0_config not needed FC fuses linear velocity and yaw rate from odometry
imu0_differential not needed FC handles this internally
imu0_relative not needed
imu0_queue_size not needed
imu0_remove_gravitational_acceleration imu.remove_gravitational_acceleration Logic is inverted: see Step 2 above
odomN_pose_rejection_threshold, odomN_twist_rejection_threshold outlier_threshold_gnss/imu/enc/hdg Both use Mahalanobis distance. RL exposes raw scalar thresholds with no guidance on chi-squared calibration per DOF: you set them manually. FC defaults are chi-squared critical values pre-calibrated to each sensor's measurement DOF.
process_noise_covariance ukf.q_position, ukf.q_velocity, etc. FC exposes named scalars instead of a 15×15 matrix
initial_estimate_covariance not configurable FC initializes automatically from first sensor readings
print_diagnostics always on FC publishes to /diagnostics at 1 Hz
robot_localization (navsat_transform_node) FusionCore Notes
frequency publish_rate
delay not needed FC handles timing automatically
magnetic_declination_radians not needed FC uses ECEF (true north), not magnetic north
yaw_offset not needed
zero_altitude publish.force_2d: true
broadcast_utm_transform not applicable FC fuses in ECEF, outputs local ENU
publish_filtered_gps not available not currently supported
use_odometry_yaw automatic FC initializes heading from motion automatically
wait_for_datum reference.use_first_fix: true true = anchor to first fix (default)
datum reference.x, reference.y, reference.z set in your output CRS coordinates

What FusionCore handles automatically

These required manual config in robot_localization:

Task robot_localization FusionCore
IMU frame → base_link transform specify imu0_config axes manually looks up TF tree automatically
GPS antenna offset (lever arm) ignored gnss.lever_arm_x/y/z applied per-fix
GPS noise estimation sensor-reported covariance, used as-is adapts from 50-sample innovation window
Zero-velocity updates (ZUPT) not built-in auto when encoder + UKF angular rate below threshold
IMU bias estimation not built-in gyro + accel bias states in the 23D state vector
GPS fix quality pre-filter not built-in gnss.max_hdop, gnss.min_satellites, gnss.min_fix_type
Measurement rejection Mahalanobis threshold (per sensor, scalar) Chi-squared gate (per sensor, calibrated to sensor DOF)
Delayed GPS fusion smooth_lagged_data + history_length IMU ring buffer replay, always on

What robot_localization has that FusionCore doesn't

Know these before you migrate:

Feature robot_localization FusionCore
Multiple independent odometry sources yes (odom0, odom1, ...) primary wheel odom + one secondary via encoder2.topic
Multiple IMUs yes (imu0, imu1, ...) primary /imu/data + one secondary via imu2.topic
Configurable state vector yes (per-sensor config booleans) fixed 23D state (position, orientation, velocity, biases, encoder WZ bias)
Arbitrary sensor plugins yes (extensible) IMU, wheel odometry, GPS only
Published filtered GPS publish_filtered_gps: true not currently supported
navsat datum from ROS service /datum service not currently supported

Services

FusionCore advertises four services: /fromLL, ~/reset, ~/save_checkpoint and ~/load_checkpoint. The comparison against robot_localization:

Service robot_localization FusionCore
/fromLL (lat/lon to map frame) robot_localization/srv/FromLL supported, same type, since 0.3.5
/fromLLArray (batch conversion) yes not supported
/toLL (map frame back to lat/lon) yes not supported
/set_datum yes not supported, use reference.x/y/z at configure time
/set_pose (jump the filter to a given pose) yes not supported, ~/reset re-initialises instead
/toggle_filter_processing yes not supported
Save and restore filter state no ~/save_checkpoint, ~/load_checkpoint

/fromLL is the one Nav2's GPS waypoint follower needs, and it is deliberately advertised with robot_localization's own type so followGpsWaypoints binds to it. See Nav2 integration for why that matters and for a known issue with the bundled Nav2 launch.


Questions? Open a GitHub Discussion.