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:


Why bother upgrading?

For your robots, the main reasons are a longer support runway and access to newer ROS/Gazebo capabilities.

Click here to see details
Reason What you gain How much it matters
Longer OS support Ubuntu 26.04 standard support runs through May 2031; 24.04 through June 2029. Useful if this will be your development machine for several years.
Longer ROS support Lyrical is an LTS supported through May 2031. Gives a new robot project more time before its next major migration.
New simulation baseline The standard pairing moves from Jazzy + Gazebo Harmonic to Lyrical + Gazebo Jetty. Useful for newer simulator fixes and features; also requires retesting sensors, bridges, and control plugins.
ROS improvements New executor options, Python asyncio support, and better programmatic bag recording/playback. Helpful when you adopt them; upgrading alone doesn’t enable every benefit.

One concrete performance opportunity: Lyrical documents 10–15% lower CPU use with its new Callback Group Events executor compared with the conventional executors. That applies to that executor—not a promise that your entire simulation becomes 15% faster.

The cost for your project is integration work. You depend on Gazebo sensors, ros_gz_bridge, ros2_control, Nav2, localization, and camera packages. Changing the OS and ROS distribution means rebuilding dependencies and checking their behavior together. Ubuntu’s .1 release consolidates early fixes, but it doesn’t certify that whole robotics stack.

Common recommendation: use 26.04.1 + Lyrical for a new development installation, while keeping a known-working Jazzy image for the physical robot until simulation, navigation, and hardware drivers pass your checks.

You might encounter bugs - a problem that either distribution can have. Upgrading won’t prevent that class of issue. Upgrading might surface hidden bugs that otherwise would remain hidden.

Upgrading Ubuntu 24.04 to 26.04.1

Here are commands to perform an 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 the console, not a "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 to make sure your machine appears on the LAN by name:

sudo apt install avahi-daemon

See Tips & Gotchas below.

You can now use a "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:

  • https://docs.ros.org/en/lyrical/Get-Started/Installation/Ubuntu-Install-Debs.html
    • Consider using plain default DDS at least at the beginning; it works fine.
    • Don't expect to see your "Lyrical" topics from a "Jazzy" workstation. They are not compatible.
    • Starting with this release, you no longer need to manage a separate Gazebo repository; Gazebo Jetty is directly included as part of the official ROS vendor packages. Installing the desktop suite ros-lyrical-desktop will automatically install Gazebo Jetty on your system.

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 you use GPS, install the 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 file, 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

Sonars and other range sensors normally publish a "range: .inf" value when there is "no return (response)" for a beam:

ros2 topic echo /sonar_F_L --once

	header:
	  stamp:
	    sec: 107
	    nanosec: 736000000
	  frame_id: sonar_F_L_frame
	radiation_type: 0
	field_of_view: 0.10000000149011612
	min_range: 0.009999999776482582
	max_range: 2.5
	range: .inf
	variance: 0.009999999776482582

Since Lyrical, Nav2 Collision Monitor rejects these messages, preventing the robot from moving. The trace shows numerous messages:

[component_container_mt-20] [ERROR] [...] [collision_monitor]: [sonar_F_L]: Malformed range message. Rejecting...
[component_container_mt-20] [ERROR] [...] [collision_monitor]: [sonar_F_R]: Malformed range message. Rejecting...

ROS2 Jazzy didn't have this problem.

See the issue and PR for the Nav2 codebase:

The PR is back-merged into Lyrical and Jazzy and will soon appear in binaries.

The rviz_attitude_plugin

The rviz_attitude_plugin doesn't come in binaries and 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
Attitude plugin

DepthAI 2.x / 3.x incompatibility

Support for OAK-D cameras is installed with:

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 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 the 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.
Battery Monitoring

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 the Seggy robot as well with its DFRobot AI Gesture and Face Detection Sensor.

Testing: on a live robot and in simulation

Solving Nav2 MPPI crashes on older Intel Core i7 Sandy Bridge CPUs

If this happens:

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

The "-4" indicates an illegal instruction crash due to incompatible 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 the installation path and version of the package installed through apt:

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

⚠️ **GitHub.com Fallback** ⚠️