Jazzy to Lyrical migration - slgrobotics/articubot_one GitHub Wiki

  • This is not a quick and painless process. You've been warned! ;-)
  • To set up a machine from scratch, see this guide.
  • Instructions apply to a Workstation or Raspberry Pi
  • When upgrading a headless Raspberry Pi, connect a keyboard and monitor. A mouse is also needed if you're running the Desktop version of Ubuntu.
  • An Ethernet connection is highly recommended. The Raspberry Pi's built-in Wi-Fi isn't your best option during an upgrade.

Contents:


Upgrading Ubuntu 24.04 to 26.04.1

Here are commands to perform in-place upgrade:

1. Clean up ROS (Jazzy, Kilted...):

sudo apt update &&  sudo apt upgrade
sudo apt purge "^ros-jazzy-.*" -y

2. Clean up build tools:

sudo apt remove python3-colcon* python3-catkin*
sudo apt autoremove --purge -y

3. Remove ROS sources used by apt:

cd /etc/apt/sources.list.d
sudo rm ros2.list 
sudo apt update && sudo apt upgrade

4. Clean up your .bashrc:

cd
vi .bashrc

5. Final checks:

lsb_release -a
df -k

6. Run the upgrade:

Use Console, not "headless" ssh login!

sudo do-release-upgrade
  • It will take a while
  • There will be a couple of splash screens waiting for your input (just press Enter)
  • Keeping your current configuration files is generally safe

After the upgrade

You may want to clean your machine:

sudo apt autoremove

In the process the over-zealous apt might wipe out your avahi-daemon

Restore it, make sure your machine appears on the LAN by name:

sudo apt install avahi-daemon

See Tips & Gotchas below.

You can now use "headless" ssh login

Setting up ROS 2 Lyrical

  • Lyrical means poetic, musical, or expressing emotion in a beautiful way.
  • Luth is a French word for lute, a stringed musical instrument resembling a small guitar.

Together, Lyrical Luth evokes the idea of a musical, poetic lute. Tell me about it... ;-)

1. Follow this guide to install ROS2 Lyrical Luth:

2. In your project directories run Rosdep:

cd ~/robot_ws
rm -rf install/ build/ log/
source /opt/ros/$ROS_DISTRO/setup.bash
#sudo rosdep init    - do it only once
rosdep update
rosdep install --from-paths src --ignore-src -r -y

This might take a while, as all dependencies are pulled.

Tip: you can always check which dependencies are not met:

cd ~/robot_ws
rosdep check --from-paths src --ignore-src --rosdistro lyrical

If using GPS, install generic driver package:

sudo apt install ros-${ROS_DISTRO}-nmea-navsat-driver

3. Now you can build your packages:

colcon build

Tips & Gotchas

The "apt" magic:

Use sudo apt clean and sudo apt update if in doubt about your current cache state.

  • sudo apt clean clears out the local repository cache of downloaded package files (.deb files stored in /var/cache/apt/archives/). It forces apt to fetch fresh packages from the remote mirror on your next install or upgrade, which is great for clearing corrupted downloads or freeing up disk space.
  • sudo apt update re-downloads the package index files from your configured repositories, giving apt a fresh list of available packages and dependencies.
  • Reset package indexes completely:
    • sudo rm -rf /var/lib/apt/lists/* && sudo apt clean && sudo apt update - clears corrupted index files if apt update throws hash sum mismatch or sync errors.

An unlikely "kernel reverts to 6.8" problem

Check your kernel after reboots, it should always be 7.0.*:

ros@seggy:~$ uname -a
Linux seggy 7.0.0-1020-raspi #20-Ubuntu SMP PREEMPT_DYNAMIC Fri Sep 11 16:02:28 UTC 2026 aarch64 GNU/Linux

If you start seeing "Linux seggy 6.8.0-1065-raspi..." - here is a brute force fix:

sudo apt install --reinstall \
    linux-image-7.0.0-1020-raspi \
    linux-raspi \
    piboot-try
  • Problem: After upgrading a Raspberry Pi 5 (Seggy) to Ubuntu 26.04.1, it kept booting the old Linux 6.8 kernel instead of the installed Linux 7.0. The piboot-try A/B boot mechanism repeatedly marked the new kernel as bad and reverted to 6.8.
  • Fix: Reinstalling the kernel and boot-management packages resolved the issue. This likely regenerated the boot assets and reset the kernel upgrade process, allowing Linux 7.0 to boot successfully.

RViz blinking fix:

export QT_ENABLE_HIGHDPI_SCALING=0

Fixing RQT:

export QT_QPA_PLATFORM=xcb

ROS Lyrical Breaking Changes

Here are some code changes that you need to apply to your packages.

Nav2 Kilted to Lyrical migration info: https://docs.nav2.org/rolling/configuration_and_development/migration_guides/kilted/Kilted/

CMakefile changes:

Lyrical ament_cmake changed. In Jazzy, ament_target_dependencies() was supplied by ament_cmake and commonly used like this:

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(sensor_msgs REQUIRED)

add_executable(bno08x_driver src/bno08x_driver.cpp)

ament_target_dependencies(bno08x_driver
  rclcpp
  sensor_msgs
)

In Lyrical, the ament_target_dependencies legacy macro has been removed from the normal ament_cmake interface.

Preferred Lyrical fix: replace

ament_target_dependencies(bno08x_driver
  rclcpp
  sensor_msgs
  geometry_msgs
)

with modern CMake target linking:

target_link_libraries(bno08x_driver PRIVATE
  rclcpp::rclcpp
  ${sensor_msgs_TARGETS}
  ${geometry_msgs_TARGETS}
)

The exact target names depend on your dependencies. For ROS interface packages, Lyrical commonly exports ${package_TARGETS}, whereas libraries such as rclcpp provide proper namespaced targets such as rclcpp::rclcpp.

New Nav2 Follower motion_model plugin:

The robots/seggy/config/nav2_params.yaml, for example, specifies a new plugin:

      motion_model: "DiffDrive"
      # Lyrical loads motion models as plugins under the selected namespace.
      DiffDrive:
        plugin: "mppi::DiffDriveMotionModel"

Sonars .inf problem in Collision Monitor

See issue and PR for Nav2 codebase:

The rviz_attitude_plugin

The rviz_attitude_plugin doesn't come in binaries, it needs minor tweaks, so I created a fork:

cd ~/robot_ws/src
git clone https://github.com/slgrobotics/rviz_attitude_plugin.git

# Lyrical only: for rviz_attitude_plugin:
sudo apt-get install -y qt6-base-dev

DepthAI 2.x / 3.x incompatibility

OAK-D cameras support is installed by:

sudo apt install ros-${ROS_DISTRO}-depthai-ros
    Installing:                     
      ros-lyrical-depthai-ros
    Installing dependencies:
      ros-lyrical-depthai-examples  ros-lyrical-depthai-filters

The old Jazzy DepthAI 2.x version is now replaced by:

  • DepthAI version: 3.10.0
  • DepthAI CMake directory: /opt/ros/lyrical/lib/aarch64-linux-gnu/cmake/depthai
  • depthai_bridge: 3.4.0

This issue is under investigation.


Battery monitoring

  • now with the official ROS 2 Lyrical broadcaster (mods and docs courtesy ChatGPT Desktop)

Fido, Seggy, Plucky and Dragger now use the official battery_state_broadcaster package (installed binaries version 6.9.0).

Their battery-enabled diffdrive_arduino driver must also be updated: the old unprefixed hardware interfaces are incompatible with this configuration.

The workstation still needs Battery Monitoring packages for displaying battery data in RViz - for simulation and real robots.

Published values and compatibility

The public topic remains battery/battery_state (sensor_msgs/msg/BatteryState), relative to the robot namespace. The header frame remains battery_frame. Existing RViz overlays and the percentage relay continue to receive a 0..1 SOC fraction. Simulation continues to use the Gazebo bridge without the adapter.

Field Result of migration
Voltage Retained, live hardware voltage.
Current Retained, live hardware current; negative while discharging.
Temperature Retained; this driver's existing constant 21 C is unchanged.
Charge (Ah) Retained, calculated by the driver from capacity and SOC.
SOC / percentage Retained, driver's existing voltage-based estimate. Public percentage is a 0..1 fraction, bounded to that range; unknown remains NaN.
Power supply status Retained, driver's existing voltage-based charging/discharging classification.
Power supply health Retained, driver's existing voltage-based health classification.
Present Retained, including false.
Capacity (Ah) Becomes static configuration. Values match the old driver's fixed estimate: 18 Ah for Fido/Seggy/Plucky, 45 Ah for Dragger. Dynamic capacity changes from hardware can no longer flow through the official broadcaster.
Design capacity Retained as configuration: 20 Ah or 50 Ah.
Chemistry, location, serial number Retained as configuration.
Cell voltages / cell temperatures Still empty; the previous broadcaster did not publish them either.

No currently produced measurement is removed. The lost capability is forwarding a changing hardware capacity estimate. The current driver did not measure capacity: it always assigned design_capacity * 0.9. If a future BMS supplies changing full-charge capacity, additional integration will be required.

SOC is not a separate BatteryState field: it is represented by percentage. The migration does not turn the driver's estimates into measured BMS values.

Local package retirement and builds

Only the local ros_battery_monitoring/battery_state_broadcaster is excluded using COLCON_IGNORE. Its modified source is preserved. We keep battery_state_rviz_overlay: it is a separate package used by the RViz only. The obsolete isolated install prefix must also be moved out of install, otherwise it can continue to override /opt/ros/lyrical.

On the robot, build the updated robot driver and articubot package together, with serial available in the underlay. After building, use a fresh terminal and source the workspace's install/setup.bash. Check resolution with:

ros2 pkg prefix battery_state_broadcaster
# Expected: /opt/ros/lyrical
ros2 pkg prefix diffdrive_arduino
# Expected: ~/robot_ws/install/diffdrive_arduino

Both sides of the interface migration must be deployed together on each robot. Do not use the old battery-free friend branches of diffdrive_arduino with these robot configurations.

Why the battery_state_compat adapter exists

battery_state_compat lets existing battery consumers use the official ROS 2 Lyrical battery broadcaster without changing their expected topic or message conventions.

The official broadcaster reads hardware interfaces exported by diffdrive_arduino, including prefixed names such as main_battery/battery_voltage and a typed Boolean battery_present interface. It uses a 0–100 percentage scale; the driver exports that scale while retaining its internal 0–1 SOC fraction for charge calculations.

The adapter:

  • Subscribes to battery/raw_battery_states, the official broadcaster’s per-battery array.
  • Selects the configured battery, normally main_battery, using its header.frame_id.
  • Converts percentage from 0–100 to the sensor_msgs/BatteryState convention of 0–1.
  • Sets header.frame_id to battery_frame.
  • Republishes the message on battery/battery_state, preserving all other fields.

Using the per-battery output preserves the actual presence flag—including false—and the original metadata strings. The official aggregate output handles these differently and remains available at battery/aggregate_state for diagnostics.

The adapter belongs to articubot_one package and runs onboard the real robot alongside the official broadcaster. This keeps existing RViz displays and other battery consumers working.

Gazebo supplies the expected message through ros_gz_bridge, so simulation does not use the adapter. All topic names above are relative to the robot namespace.

Data flow

For the real Seggy robot, the configured connections are:

Topic Publisher Subscriber / purpose
/battery/raw_battery_states Official battery_state_broadcaster battery_state_compat selects main_battery from this array.
/battery/aggregate_state Official battery_state_broadcaster Available for diagnostics.
/battery/battery_state battery_state_compat battery_state_rviz_overlay and battery_pie_chart_relay. This is the compatible battery message with percentage on the 0–1 scale.
/battery/percentage battery_pie_chart_relay (topic_tools/relay_field) Intended for a battery percentage "Pie Chart" display. It contains only the percentage as std_msgs/Float32.
/battery_display_text battery_state_rviz_overlay RViz’s Battery Status text overlay. Contains formatted display text and its appearance settings.
/battery_state_broadcaster/transition_event The broadcaster’s lifecycle infrastructure For lifecycle monitoring tools.

Note: Turtle’s battery monitoring is unaffected by this migration.

  • Real hardware: Turtle launches create_driver directly. It doesn’t launch the official battery broadcaster or battery_state_compat, and doesn’t use diffdrive_arduino.
  • Gazebo: Battery messages still come through ros_gz_bridge; the adapter is bypassed.
  • RViz: The existing battery overlay and percentage relay remain unchanged.

Turtle does include the shared battery.xacro, which changed. Both modes expand correctly: simulation retains its Gazebo battery plugin, while hardware mode adds no battery control interfaces.

Validation

The migration was built against ROS 2 Lyrical. Python tests cover normalization, unknown SOC, field preservation and battery selection; C++ tests cover exported percentage and bool presence interfaces and encoder regression. Hardware and simulation xacros were expanded for all four robots. A separate integration check linked the installed official broadcaster and verified publication of all configured fields, false presence and unknown SOC with supplied readings.

Behavior trees

ROS 2 Lyrical migration

Changes to slg_bt_plugins package

  • Migrated BT blackboard nodes to nav2::LifecycleNode::SharedPtr and updated ROS factories and dependencies.
  • Fixed cancellation response processing and timeouts, including support for trees without a perception pump.
  • Corrected continuous monitoring and reactive face/gesture handling.
  • Added a pump-based face_aware_nav.xml, installed the example trees, and exposed USE_RCLCPP_SUBSCRIPTIONS as a CMake option.
  • Added regression tests for plugin construction, cancellation, and BT control flow.

Changes to articubot_one package BTs

  • Updated Seggy’s navigation trees to reference the correct Lyrical Nav2 subtree.
  • Removed control structures that masked navigation success or failure.
  • Changed STOP handling to cancel the goal and prevent automatic resumption when STOP clears.
  • Made face detection pause navigation, with alignment in face_aware_nav.xml and resumption when the face disappears.
  • Fixed the minimal tree’s perception updates and false goal completion.

Documentation and validation

  • Corrected outdated descriptions.
  • Both packages build successfully; default-mode, subscription-mode, and Seggy control-flow tests pass.
  • Full Nav2 simulation confirmed in Seggy simulation:
cd ~/robot_ws
colcon build
ros2 launch articubot_one seggy_sim.launch.py
  • Works on Seggy robot as well with its DFRobot AI Gesture and Face Detection Sensor.

Testing: live robot and in simulation

Solving Nav2 MPPI crashes on older Intel I7 Sandbridge CPUs

If this happens:

[ERROR] [component_container_mt-...]: process has died ..., exit code -4, ...

The "-4" indicates illegal instruction crash, due to uncompatible optimization flags in the installed binaries.

Problem: MPPI was compiled with -mfma, enabling instructions unsupported by your i7-2600K (or similar) and causing an “Illegal instruction” crash.

Solution: Removing -mfma and rebuilding MPPI for Sandy Bridge fixed it. No Nav2 YAML changes were needed.

Check your CPU:

lscpu | grep -E 'Model name|Flags'

Check installed by apt ros-lyrical-nav2-mppi-controller path and version:

ros2 pkg prefix nav2_mppi_controller
apt-cache policy ros-lyrical-nav2-mppi-controller

Here is how to build nav2_mppi_controller locally without -mfma optimization:

mkdir -p ~/nav2_compat_ws/src
cd ~/nav2_compat_ws/src

# clone what hopefully matches apt-installed version:
git clone https://github.com/ros-navigation/navigation2.git -b lyrical

vi ~/nav2_compat_ws/src/navigation2/nav2_mppi_controller/CMakeLists.txt

        # ------ remove or comment out the block (after line 28): ----------

	#check_cxx_compiler_flag("-mfma" COMPILER_SUPPORTS_FMA)

	#if(COMPILER_SUPPORTS_FMA)
	#  add_compile_options(-mfma)
	#endif()

source /opt/ros/lyrical/setup.bash
cd ~/nav2_compat_ws

colcon build --packages-select nav2_mppi_controller \
  --cmake-clean-cache \
  --cmake-args \
    -DCMAKE_BUILD_TYPE=Release \
    -DBUILD_TESTING=OFF \
    -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
    '-DCMAKE_CXX_FLAGS=-march=sandybridge -mtune=sandybridge'

# you may want to have this in your ~/.bashrc:    
source /opt/ros/lyrical/setup.bash
source ~/nav2_compat_ws/install/setup.bash
source ~/robot_ws/install/setup.bash

# check it:
ros2 pkg prefix nav2_mppi_controller

Now you can run your robot simulation on an old workstation, and Nav2 should not crash with the "-4":

cd ~/robot_ws
colcon build; ros2 launch articubot_one fido_sim.launch.py

Back to the Top