ROS orchestration with snaps

gbeuzeboc

on 22 September 2022

This article was last updated 1 year ago.


Application orchestration is the process of integrating applications together to automate and synchronise processes. In robotics, this is essential, especially on complex systems that involve a lot of different processes working together. But, ROS applications are usually launched all at once from one top-level launch file.

With orchestration, smaller launch files could be launched and synchronised to start one after the other to make sure everything is in the right state. Orchestration can also hold processes and insert some process logic. This is what ROS orchestration should be about.

This way, for instance, you could make your localisation node start only once your map_server made the map available.

Snaps offer orchestration features that might come handy for your ROS orchestration.

In this post, we will demonstrate how to start a snap automatically at boot and how to monitor it. Then, through some examples, we will explore the different orchestration features that snaps offer. We thus assume that you are familiar with snaps for ROS; if you aren’t, or need a refresher, head over to the documentation page.

Let’s get started

Let us first build and install the snap we will use in this step-by-step tutorial

git clone https://github.com/ubuntu-robotics/ros-snaps-examples.git -b ros_orchestration_with_snaps_blog
cd ros-snaps-examples/orchestration_humble_core22
snapcraft
sudo snap install talker-listener_0.1_amd64.snap --dangerous 

Note that all the steps described hereafter are already implemented in this git repository. However, they are commented for you to easily follow along.

Start a ROS application automatically at boot

Once you have tested and snapped your robotics software, you can start it from the shell. For an autonomous robot, starting your applications automatically at boot is preferable than starting manually every single time. It obviously saves time and most importantly makes your robot truly autonomous.

Snaps offer a simple way to turn your snap command into services and daemons, so that they will either start automatically at boot time and end when the machine is shut down, or start and stop on demand through socket activation.

Here, we will work with a simple ROS 2 Humble talker-listener that is already snapped (strictly confined). If you want to know how the talker-listener was snapped, you can visit the How to build a snap using ROS 2 Humble blog post.

Turn your snap command into a daemon

Once you have snapped your application, you can not only expose commands, but also create daemons

  • Daemons are commands that can be started automatically at boot, which is a must-have for your robot software.

For now, our snap is exposing two commands – talker and listener. They respectively start the node publishing message and the node subscribing and listening to the message.

You can test the snap by launching each of the following commands in their own terminal:

$ talker-listener.talker
$ talker-listener.listener

In order to start them both automatically in the background, we must turn them into daemons. Snap daemons can be of different types, but the most common one is “simple”. It will simply run as long as the service is enabled.

To turn our application into daemons, we only have to add ‘daemon: simple’ to both our snap applications:

apps: 
  listener:
    command: opt/ros/humble/bin/ros2 run demo_nodes_cpp listener 
    + daemon: simple
    plugs: [network, network-bind]
    extensions: [ros2-humble]

  talker: command: opt/ros/humble/bin/ros2 run demo_nodes_cpp talker
    + daemon: simple
    plugs: [network, network-bind]
    extensions: [ros2-humble]

All there’s left to do is to rebuild and re-install the snap. Upon installation, both daemons are going to be automatically started in no particular order

Now we can build and install this snap.

Check your daemons

Now our talker and listener are running in the background. Snaps offer a way to monitor and interact with your snap daemons.

Snap daemons are actually plain SystemD daemons so if you are familiar with SystemD commands and tools (systemctl, journalctl, etc.) you can use them for snap daemons too.

For this post, we are going to focus on snap commands to interact with our daemons and monitor them.

Check our service status

The very first thing would be to verify the status of our daemons, making sure they are running. The snap info command gives us a summary of the status of our daemons,

$ snap info talker-listener

name: talker-listener
summary: ROS 2 Talker/Listener Example
publisher: –
license: unset
description: | 
 This example launches a ROS 2 talker and listener.
Services:
 talker-listener.listener: simple, enabled, active
 talker-listener.talker: simple, enabled, active
refresh-date: today at 18:00 CEST 
installed: 0.1 (x35) 69MB -

Here we see our two services listed. They are both simple, enabled and active.

Simple is the type of daemon we specified. Enabled, means that our service is meant to start automatically (at boot, upon snap install etc). Active, means that our service is currently running.

So here, both our talker and listener services are up and running. Let’s browse the logs.

Browsing the logs

The snap command also offers a way to browse our service logs.

Since our services are already running in the background, we can type:

$ sudo snap logs talker-listener

2022-08-23T11:13:08+02:00 talker-listener.listener [2833606]: [INFO] [1661245988.120676423] [talker]: Publishing: 'Hello World: 123' 
[...]
2022-08-23T11:13:12+02:00 talker-listener.talker[2833607]: [INFO] [1661245992.121411564] [listener]: I heard: [Hello World: 123]

This command will fetch the logs of our services and display the last 10 lines by default. In case you want the command to continuously run and print new logs as they come in, you can use the “-f” option:

sudo snap logs talker-listener -f

Note that so far we have been fetching the logs of our whole snap (both services). We can also get the logs of a specific service. To continuously fetch the listener logs, type:

sudo snap logs talker-listener.listener -f

Interact with snap daemons

The snap command also offers ways to control services. As we saw, our services are currently enabled and active.

Enabled, means our service will start automatically at boot. We can change this by “disabling” it, so it won’t start automatically any more

sudo snap disable talker-listener.talker

Note that disabling the service won’t stop the current running process.

We can also stop the current process altogether with:

sudo snap stop talker-listener.talker

Conversely, we can enable/start a service with:

sudo snap enable talker-listener.talker
sudo snap start talker-listener.talker

Make sure to re-enable everything to keep following this post along:

sudo snap enable talker-listener

ROS orchestration

So far, our talker and listener start up without any specific orchestration; or in layman’s terms, in no specific order. Fortunately, snaps offer different ways to orchestrate services.

To spice up our experience, let’s add some script to our snap to showcase the orchestration features:

Parts:
[...]
  + # copy local scripts to the snap usr/bin 
  + local-files:
    + plugin: dump
      + source: snap/local/
      + organize: '*.sh': usr/bin/

This is a collection of bash scripts that have been conveniently prepared to demonstrate orchestration hereafter.

We will also add another app:

Apps:
  [...]
  + listener-waiter:
    + command: usr/bin/listener-waiter.sh
    + daemon: oneshot
    + plugs: [network, network-bind]
    + extensions: [ros2-humble]
    + # Necessary for python3 ROS app
    + environment: "LD_LIBRARY_PATH": "$LD_LIBRARY_PATH:$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/blas:$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/lapack"

This app is simply waiting for the node /listener to be present. This daemon is created as a “oneshot”, another type of daemon that is meant to only be run once at start and then exit after completion.

After and before for ROS orchestration

The very first thing we can do is to change the start order. The after/before keywords are valid only for daemons and allow us to specify if a specific daemon should be started after or before one (or several) other service(s). Note that for oneshot daemons, the before/after keywords are waiting for the completion of the oneshot service.

The scenario here goes as follows: start the listener, make sure it’s properly started, then and only then, start the talker. To make sure our listener is properly started, we will use the listener-waiter app we introduced in the previous section. Remember, it waits for a node to be listed.

Here, we define the orchestration only at the listener-waiter application level to keep it simple. So, we want it to start after the listener and before the talker so the talker will start only once the listener is ready.

To do so, let’s use the before/after keywords:

listener-waiter:
      command: usr/bin/listener-waiter.sh
      daemon: oneshot
      + after: [listener]
      + before: [talker]
      plugs: [network, network-bind]
      extensions: [ros2-humble]

This is rather explicit, it must be started after the listener but before the talker. After rebuilding the snap, we can reinstall it and look at the log again. Here is a shortened version of the output logs:

systemd[1]: Started Service for snap application talker-listener.listener. 
systemd[1]: Starting Service for snap application talker-listener.listener-waiter... talker-listener.listener-waiter[76329]: Making sure the listener is started 
systemd[1]: snap.talker-listener.listener-waiter.service: Succeeded. 
systemd[1]: Finished Service for snap application talker-listener.listener-waiter. systemd[1]: Started Service for snap application
talker-listener.talker. [talker]: Publishing: 'Hello World: 1'
talker-listener.listener[76439]: [INFO] [1661266809.685248681] [listener]: I heard: [Hello World: 1]

We can see in this log that everything went as expected. The talker has been started only once the listener was available.

In this example, we specified the before/after field within the listener-waiter for the sake of simplicity. However, any daemon can specify a before/after as long as the specified applications are from the same snap, allowing for pretty complex orchestration.

Stop-command for ROS orchestration 

Another interesting feature for snap orchestration is the stop-command. It allows one to specify a script, or a command, to be called right before the stop signal is sent to a program when running snap stop. With this, we could make sure, for instance, that everything is synchronised or saved before exiting. Let’s look at a quick example: running an echo of a string.

A script called stop-command.sh has already been added to the snap usr/bin.

All we need to do here is to specify the path to the said script as a stop-command.

talker:
  command: opt/ros/humble/bin/ros2 run demo_nodes_cpp talker
  plugs: [network, network-bind]
  daemon: simple
  + stop-command: usr/bin/stop-command.sh
  extensions: [ros2-humble]

After rebuilding and reinstalling the snap, we can trigger a stop manually with the snap stop command:

sudo snap stop talker-listener # stop all the services of this snap
sudo snap logs talker-listener -f # visualize the logs

We should see an output similar to:

systemd[1]: Stopping Service for snap application talker-listener.listener... 
systemd[1]: snap.talker-listener.listener.service: Succeeded.
systemd[1]: Stopped Service for snap application talker-listener.listener.
systemd[1]: Stopping Service for snap application talker-listener.talker... 
talker-listener.talker[86863]: About to stop the service
systemd[1]: snap.talker-listener.talker.service: Succeeded. 2022-08-23T17:23:57+02:00 systemd[1]: Stopped Service for snap application talker-listener.talker.

From the logs, we can see that before exiting the service talker, the stop-command script was executed and printed a message: “About to stop the service”. Then, only after the stop-command script finished, was the talker terminated.

Post-stop-command for ROS orchestration

Similarly to the stop-command entry, the post-stop-command is also calling a command, but this time, only after the service is stopped.

The use case could be to run some data clean-up or even notify a server that your system just stopped. Again, let us try this feature with a conveniently pre-baked script logging a message

talker:
  command: opt/ros/humble/bin/ros2 run demo_nodes_cpp talker
  plugs: [network, network-bind]
  daemon: simple
  stop-command: usr/bin/stop-command.sh
  + post-stop-command: usr/bin/post-stop-command.sh
  extensions: [ros2-humble]

Rebuild, re-install, and without much surprise, we get the following output:

systemd[1]: Stopping Service for snap application talker-listener.talker...
talker-listener.talker[90660]: About to stop the service
talker-listener.talker[90548]: [INFO] [1661269138.094854527] [rclcpp]: signal_handler(signum=15)
talker-listener.talker[90710]: Goodbye from the post-stop-command!
systemd[1]: snap.talker-listener.talker.service: Succeeded.
systemd[1]: Stopped Service for snap application talker-listener.talker.

From the logs we can see that our talker application executed the stop-command script then received the termination signal and only after our post-command script logged the message: ”Goodbye from the post-stop-command!”.

Command-chain

So far, we have seen how to call additional commands around the moment we stop our service. The command-chain keyword allows us to list commands to be executed before our main command. The characteristic use case is to set up your environment. The ros2-humble extension that we are using in our snap example is actually using this mechanism. Thanks to it, we don’t have to worry about sourcing the ROS environment in the snap. If you are curious, here is the said command-chain script. The best part is that the command-chain entry is not only available for daemons, but also for services and regular commands.

The scripts listed in the command-chain are not called one by one automatically. Instead, they are called as arguments of each others, resulting in a final command similar to:

./command-chain-script1.sh command-chain-script2.sh main-script.sh

So you must make sure that your command-chain-scripts are calling passed arguments as executables. For example, here, command-chain-script1.sh is responsible for calling command-chain-script2.sh.

Let’s see what our command-chain-talker.sh script looks like

#!/usr/bin/sh
echo "Hello from the talker command-chain!"

# Necessary to start the main command 
exec $@

The only thing to pay attention to is the exec $@ which is simply calling the next command. If we don’t specify this, our main snap command won’t be called.

Let’s add yet another script to our talker command-chain:

talker:
  + command-chain: [usr/bin/command-chain-talker.sh]
  command: opt/ros/humble/bin/ros2 run demo_nodes_cpp talker
  plugs: [network, network-bind]
  daemon: simple
  stop-command: usr/bin/stop-command.sh
  post-stop-command: usr/bin/post-stop-command.sh
  extensions: [ros2-humble]

After building and installing, we can see that now the logs are:

systemd[1]: Starting Service for snap application talker-listener.listener-waiter... talker-listener.listener-waiter[96438]: Making sure the listener is started 
systemd[1]: snap.talker-listener.listener-waiter.service: Succeeded. 
systemd[1]: Finished Service for snap application talker-listener.listener-waiter.
systemd[1]: Started Service for snap application talker-listener.talker.
talker-listener.talker[96538]: Hello from the talker command-chain!
talker-listener.talker[96594]: [INFO] [1661271361.139378609] [talker]: Publishing: 'Hello World: 1'

We can see from the logs that once our listener was available, the talker part was started. The command-chain-talker.sh script was called and printed the message:  “Hello from the talker command-chain!”, and only after that our talker started publishing.

Conclusion

I hope that reading this article helps you understand the snap daemon features a bit more and inspires you to use them for ROS orchestration. For now, orchestration can only be done within the same snap, since strictly confined snaps are not allowed to launch other applications outside their sandbox. Of course, you could also combine the snap orchestration features with other orchestration software. Most notably, ROS 2 nodes lifecycle allows you to control the state of your nodes, so that you can orchestrate your node’s initialisation, for instance.
If you have any feedback, questions or ideas regarding ROS snap orchestration with snaps, please join our forum and let us know what you think. Furthermore, have a look at the snap documentation if you want to learn more about snaps for robotics applications.

Talk to us today

Interested in running Ubuntu in your organisation?

Newsletter signup

Get the latest Ubuntu news and updates in your inbox.

By submitting this form, I confirm that I have read and agree to Canonical's Privacy Policy.

Are you building a robot on top of Ubuntu and looking for a partner? Talk to us!

Contact Us

Related posts

Optimise your ROS snap – Part 2

Welcome to Part 2 of the “optimise your ROS snap” blog series. Make sure to check Part 1 before reading this blog post. This second part is going to present...

Optimise your ROS snap – Part 1

Do you want to optimise the performance of your ROS snap? We reduced the size of the installed Gazebo snap by 95%! This is how you can do it for your snap....

Optimise your ROS snap – Part 6

Welcome to Part 6 of our “Optimise your ROS snap” blog series. Make sure to check Part 5. This sixth and final part will  summarise every optimisation that we...