Distributing a ROS system among multiple snaps

by Kyle Fazzari on 22 March 2017

One of the key tenets of snaps is that they bundle their dependencies. The fact that they’re self-contained helps their transactional-ness: upgrading or rolling back is essentially just a matter of unmounting one snap and mounting the other. However, historically this was also one of their key downsides: every snap must be standalone. Fortunately, snapd v2.0.10 saw the addition of a content interface that could be used by a producer snap to make its content available for use by a consumer snap. However, that interface was very difficult to utilize when it came to ROS due to ROS’s use of workspaces for both building and running. At long last, support is landing in Snapcraft for building a ROS system that is distributed among multiple snaps, and I wanted to give you a preview of what that will look like.

Why would you want to do that?

Like I said, snaps bundling their dependencies is typically a good thing, and this applies to ROS-based snaps as well. Having an entire ROS system in a single snap that updates transactionally is awesome, and useful for most deployment cases. However, there are some use-cases where this breaks down.

For example, say I’m manufacturing an unmanned aerial vehicle. I want to sell it in such a state that it’s only capable of being piloted via remote control. This is done with a ROS system, which in a simple world would be made up of:

  • One node to act as a driver for the RC radio
  • One node to drive the motors
  • Launch file to connect the two

You get the idea. In addition to that basic platform, I want my users to be able to buy add-on packs. For example, perhaps the vehicle includes a GPS sensor (as well as basic pose sensors). I’d like to sell an add-on pack that adds a very basic “fly here” autopilot, or perhaps a “follow me” mode. That’s another ROS system, perhaps something like:

  • One node to act as a driver for the GPS
  • One node (or perhaps a few) to act as a driver for the pose sensors
  • One node to plan a path
  • One node to take the path and turn it into motor controls
  • A launch file to bring up this system

If we build both of these snaps to be standalone, we quickly run into issues:

  • Lots of duplication between them, as the autopilot snap will need to include most of the base behavior snap
  • They both include (and will try to launch) their own roscore
  • The duplicated snaps in each will try to access their respective hardware. This is a race condition: the first one up will win, the second will die. Or, depending on the hardware interface, they’ll both control it. That’s fun.

Using content sharing, we can actually make the autopilot snap depend upon and utilize the base behavior snap.

Alright, what does this look like?

Disclamer: The ideas presented in this blogposts are still valid, but the code examples given here are only valid for the base: core

Let’s simplify our previous example into two snaps: a “ros-base” snap that includes the typical stuff: roscore, roslaunch, etc., and a “ros-app” snap that includes packages that actually do something, specifically the classic talker/listener example. A quick reminder: this will only be possible in Snapcraft v2.28 or later.

Create ros-base

To create the base snap, create a snap/snapcraft.yaml file with the following contents:

name: ros-base
version: '1.0'
grade: stable
confinement: strict
summary: ROS Base Snap
description: Contains roscore and basic ROS utilities.

slots:
  # This is how we make a part of this snap readable by other
  # snaps. Consumers will need to access the PYTHONPATH as well
  # as various libs contained in this snap, so share the entire
  # $SNAP, not just the ROS workspace.
  ros-base:
    content: ros-base-v1
    interface: content
    read: [/]

parts:
  ros-base:
    plugin: catkin
    rosdistro: kinetic
    include-roscore: true
    catkin-packages: [] 

That’s it. Run snapcraft on it, and after a little time you’ll have your base snap (the “provider” snap regarding content sharing). This particular example doesn’t do a whole lot by itself, so let’s move on to our ros-app snap (the “consumer” snap regarding content sharing).

Create ros-app

The starting point for ros-app is the current standalone ROS demo. We’ll use the exact same ROS workspace, but we’ll add a few more things and tweak the YAML a bit.

The recommended way to build a “consumer” snap (assuming it has a build-time dependency on the content shared from the “producer” snap, which ros-app does indeed have on ros-base ) is to create a tarball of the producer’s staging area, and use it as a part to build the consumer.

Concretely, we can tar up the staging area of ros-base and use it to build ros-app , but then filter it out of the final ros-app snap (so as to not duplicate the contents of ros-base ).

So let’s do that now. cd into the directory containing the now-built ros-base snap, tar up its staging area, then move it off into the ros-app area:

$ tar czf ros-base.tar.bz2 stage/
$ mv ros-base.tar.bz2 /path/to/ros-app

Now, in /path/to/ros-app alter the snap/snapcraft.yaml to look something like this:

name: ros-app
version: '1.0'
grade: stable
confinement: strict
summary: ROS App Snap
description: Contains talker/listener ROS packages and a .launch file.

plugs:
  # Mount the content shared from ros-base into $SNAP/ros-base
  ros-base:
    content: ros-base-v1
    interface: content
    target: /ros-base

apps:
  launch-project:
    command: run-system
    plugs: [network, netwo
 rk-bind, ros-base]

parts:
  # The `source` here is the tarred staging area of the ros-base snap.
  ros-base:
    plugin: dump
    source: ros-base.tar.bz2
    # This is only used for building-- filter it out of the final snap.
    prime: [-*]

  # This is mostly unchanged from the standalone ROS example. Notable
  # additions are:
  #  - Using Kinetic now (other demo is Indigo)
  #  - Specifically not including roscore
  #  - Making sure we're building AFTER our underlay
  #  - Specifying the build- and run-time paths of the underlay
  ros-app:
    plugin: catkin
    rosdistro: kinetic
    include-roscore: false
    underlay:
      # Build-time location of the underlay
      build-path: $SNAPCRAFT_STAGE/opt/ros/kinetic

      # Run-time location of the underlay
      run-path: $SNAP/ros-base/opt/ros/kinetic
    catkin-packages:
      - talker
      - listener
    after: [ros-base]

  # We can't just use roslaunch now, since that's contained in the
  # underlay. This part will tweak the environment a little to
  # utilize the underlay.
  run-system:
    plugin: dump
    stage: [bin/run-system]
    prime: [bin/run-system]

  # We need to create the $SNAP/ros-base mountpoint for the content
  # being shared.
  mountpoint:
    plugin: nil
    install: mkdir $SNAPCRAFT_PART_INSTALL/ros-base 

Other than the ROS workspace in src/ (which remains unchanged from the other demo so we won’t discuss it here), we need to create a bin/run-system executable that looks something like this:

#!/bin/bash

# Would sure be nice if snapd gave us the triplet as well.
# It doesn't, so we'll just create it here. I'm only adding
# support for amd64 here, but one could add this logic for
# any arch they wanted (assuming ROS builds there, of
# course).
case $SNAP_ARCH in
amd64)
    export TRIPLET=x86_64-linux-gnu
    ;;
*)
    echo "Unsupported arch: $SNAP_ARCH"
    exit 1
    ;;
esac

export ROS_BASE=$SNAP/ros-base

# Add ros-base to the PYTHONPATH
export PYTHONPATH=$PYTHONPATH:$ROS_BASE/usr/lib/python2.7/dist-packages

# Add ros-base to LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ROS_BASE/lib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ROS_BASE/lib/$TRIPLET
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ROS_BASE/usr/lib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ROS_BASE/usr/lib/$TRIPLET

roslaunch listener talk_and_listen.launch

Why is this needed? Because the Catkin plugin can only do so much for you. The ros-base snap includes various python modules and libs outside of its ROS workspace that ros-app needs, so we extend the PYTHONPATH and LD_LIBRARY_PATH to utilize them.

From there, it’s as easy as running roslaunch (which by the way is contained in ros-base).

Run snapcraft on this, and after a few minutes (fairly quick since it’s re-using the base’s staging area to build) you’ll have a ros-app snap

So now I have two ROS snaps. Now what?

You now have your ROS system split between multiple snaps. The first step is to install both snaps:

$ sudo snap install --dangerous ros-base_1.0_amd64.snap
ros-base 1.0 installed
$ sudo snap install --dangerous ros-app_1.0_amd64.snap
ros-app 1.0 installed

Now take a look at snap interfaces :

$ snap interfaces
Slot                      Plug
ros-base:ros-base         -
:alsa                     -
:avahi-observe            -
...
-                         ros-app:ros-base 

You’ll see that ros-base:ros-base is an available slot, and ros-app:ros-base is an available plug. This interface is currently not connected, so content sharing is not yet taking place. Let’s connect them:

$ sudo snap connect ros-app:ros-base ros-base:ros-base

Taking another look at snap interfaces you can see they’re now connected:

$ snap interfaces
Slot                      Plug
ros-base:ros-base         ros-app
:alsa                     -
:avahi-observe            -
...

And now you can launch this ROS system you now have distributed between two snaps:

$ ros-app.launch-project

NODES
  /
    listener (listener/listener_node)
    talker (talker/talker_node)

process[talker-2]: started with pid [10649]
process[listener-3]: started with pid [10650]
[ INFO] [1487121136.757225517]: Hello world 0
[ INFO] [1487121136.860879281]: Hello world 1
[ INFO] [1487121136.960885723]: Hello world 2
[ INFO] [1487121137.057481265]: Hello world 3
[INFO] [1487121137.058298]: I heard Hello world 3

Conclusion

Multiple ROS users have mentioned that the fact that a ROS snap must be completely self-contained is a problem. Typically it either interferes with their workflow or their business plan. We’ve heard you! We can’t pretend that the snap world of isolated blobs and the ROS world of workspaces merge perfectly, but the content interface takes a big step toward blending these two worlds, and the new features in Snapcraft’s Catkin plugin hopefully makes it as easy as possible to utilize.

I personally look forward to seeing what you do with this!

Original post can be found here

Newsletter Signup

Related posts

Managing software in complex network environments: the Snap Store Proxy

As enterprises grapple with the evolving landscape of security threats, the need to safeguard internal networks from the broader internet is increasingly important. In environments with restricted internet access, it can be difficult to manage software updates in an easy, reliable way. When managing devices in the field, change management […]

Creating Snaps on Ubuntu Touch

This article was written in collaboration with Alfred E. Neumayer of the UBports Project. Tablets, phones and current technology’s capabilities are phenomenal. Who would have thought a thin, light, barely 10 inch device would provide all the power necessary to run Virtual Machines, wherever one desires while powered on battery? That a sma […]

We wish you RISC-V holidays!

There are three types of computer users: the end user, the system administrator, and the involuntary system administrator. As it happens, everyone has found themselves in the last group at some point or another; you sit down to perform a task relevant to your needs or duties, but suddenly the machine does not work as […]