Skip to content

Getting Started

Prerequisites

  • ROS 2 Jazzy Jalisco (Ubuntu 24.04) or Humble Hawksbill (Ubuntu 22.04)
  • A colcon workspace (~/ros2_ws)

Install

FusionCore is a monorepo with these packages:

Package Purpose Required?
compass_msgs Custom heading message type Yes
fusioncore_core Pure C++ UKF library, no ROS Yes
fusioncore_ros ROS 2 lifecycle node Yes
fusioncore_gazebo Simulation world + demo launch Optional
fusioncore_ublox u-blox NavPVT Doppler bridge Optional (only builds with ublox_msgs installed)

The repo must live inside src/ for colcon to find them.

Humble users

Replace jazzy with humble in all commands below.

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
git clone https://github.com/manankharwar/fusioncore.git
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-up-to fusioncore_ros
source install/setup.bash

Run colcon from the workspace root, not from inside the repo

Always run colcon build from ~/ros2_ws, not from ~/ros2_ws/src/fusioncore. colcon discovers packages by scanning src/ from the workspace root. Running it from inside the repo produces a nested install/ that conflicts with the workspace.

Headless machines (Raspberry Pi, servers)

--packages-up-to fusioncore_ros already skips Gazebo and the ublox bridge. To be explicit:

touch ~/ros2_ws/src/fusioncore/fusioncore_gazebo/COLCON_IGNORE
touch ~/ros2_ws/src/fusioncore/fusioncore_ublox/COLCON_IGNORE


Run the tests

cd ~/ros2_ws
colcon build --packages-select fusioncore_core --cmake-args -DBUILD_TESTING=ON
colcon test --packages-select fusioncore_core
colcon test-result --verbose

Expected: 272 tests, 0 errors, 0 failures, 0 skipped

That is the figure colcon test-result --all prints. It counts each package's CTest wrapper entry alongside the individual cases, so it is 27 higher than the number of actual test cases. The individual count is 245: 184 in fusioncore_core, 37 in fusioncore_ros, 24 in fusioncore_ublox. Both are correct, they answer different questions. If your number differs, re-run and compare per package rather than on the total, since one skipped suite is easier to spot that way.


First launch

FusionCore is a lifecycle node. It needs to be configured (load params, validate TF) and then activated (start processing) before it does anything.

The provided launch files handle this automatically:

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

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

Both launch files bring the node all the way up to active on their own. There is nothing else to run.

If you would rather drive the lifecycle yourself, pass autoconfigure:=false and do the transitions by hand:

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

ros2 lifecycle set /fusioncore configure
ros2 lifecycle set /fusioncore activate

Do not put FusionCore in a nav2_lifecycle_manager node list. It is a lifecycle node, so this looks like the obvious thing to do, and it fails. The manager requires each node it owns to keep a bond heartbeat open, FusionCore does not implement that protocol, and the manager therefore reports a bond timeout on every boot no matter what bond_timeout is set to. Reported from the Sowbot agricultural stack, which worked it out the hard way.

Use one of the two supported approaches instead: leave autostart on and let the node bring itself up (what both shipped launch files do), or drive the transitions from your own launch file, which is what fusioncore_nav2.launch.py does with EmitEvent.

Verify it's publishing:

ros2 topic hz /fusion/odom
# expected: average rate: 100.000

If a lifecycle command fails

"Unknown transition requested" means the node is already past the state you asked for. The launch files bring it up to active themselves unless you pass autoconfigure:=false, so there is nothing left to drive. Check where it actually is with ros2 lifecycle get /fusioncore.

"Node not found" is a different problem: DDS discovery has not settled yet, which happens on WSL2 and slow machines. Wait a couple of seconds and retry, or let the launch file do it, since its timed events are not affected.


Verify it works (single command)

bash tools/quick_test.sh

Starts FusionCore with fake sensors, runs through configure → activate, checks all outputs. Takes about 15 seconds. Prints [PASS] / [FAIL] for each check.

Expected output:

  FusionCore Quick Test
  =====================

  [....] Sourcing ROS environment...
  [PASS] ROS environment sourced
  [....] Starting TF publishers...
  [....] Launching FusionCore...
  [PASS] Lifecycle: configure → activate
  [....] Publishing fake IMU at 100 Hz (stationary, gravity pointing up)...
  [....] Publishing fake wheel odometry at 50 Hz (stationary)...
  [....] Waiting 6 s for filter to initialize...

  Checks:
  -------
  [PASS] /fusion/odom publishing (main output)
  [PASS] /fusion/pose publishing
  [PASS] /diagnostics publishing
  [PASS] /fusioncore/reset service responds

  All checks passed. FusionCore is working correctly.

If a check fails, the script prints the exact diagnostic command to run next.

Docker

The script also runs inside the container:

docker run --rm -it ghcr.io/manankharwar/fusioncore:latest bash tools/quick_test.sh


Manual verification (if you want to inspect each output)

If you want to observe the filter behavior directly rather than run the automated check:

# Terminal 1: launch
source /opt/ros/jazzy/setup.bash && source ~/ros2_ws/install/setup.bash
ros2 launch fusioncore_ros fusioncore.launch.py

# Terminal 2: TF (the launch already brought the node up to active)
source /opt/ros/jazzy/setup.bash && source ~/ros2_ws/install/setup.bash
ros2 run tf2_ros static_transform_publisher --frame-id base_link --child-frame-id imu_link &
ros2 run tf2_ros static_transform_publisher --frame-id odom --child-frame-id base_link &

# Terminal 3: diagnostics
ros2 topic echo /diagnostics --once
ros2 topic hz /fusion/odom
ros2 topic echo /fusion/odom --field twist.twist.linear

You should see /fusion/odom, /fusion/pose, and /fusioncore/reset in the topic/service list.