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:
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:
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)¶
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:
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.