IoT GUI snaps made easy

The following assumes some familiarity with using snapcraft to package snaps, and concentrates on the specifics of snapping graphical snap intended to work with, for example, the ubuntu-frame snap on Ubuntu Core.

This is a peek “under the hood” of the iot-example-graphical-snap examples. If you are looking for a “how to” about packaging an IoT GUI based on a common graphics toolkit then you should look at the " Packaging applications as IoT GUIs section of Ubuntu Frame How-to Guides.

IoT GUIs

Essentially IoT GUI snaps are Wayland applications packaged as a snap. Compared with running applications on a desktop, these do not care about integration with desktop themes and are expected to run as single fullscreen applications.

There are various types of application that fall into this category. As well as bespoke “kiosks” e.g. wpe-webkit, there are also things that might be embedded e.g. Kodi and games e.g. scummvm.

Dancing with Wayland, Dancing with Daemons

The iot-example-graphical-snap repository incorporates the experience gained writing a number of snaps to work with Ubuntu Frame and simplified the process. The result of this is a simple approach to snapping an IoT GUI.

The “wayland interface dance”

Snaps use “interfaces” to access capabilities of the system and one of these is the wayland interface. Snaps have their own $XDG_RUNTIME_DIR but Wayland expects to use a file in this location to connect to the server. As a result, every Wayland based snap needs logic to make this work.

After writing this logic a few times, we extracted it into a wayland-launch helper script:

#!/bin/sh
set -e

for PLUG in %PLUGS%; do
  if ! snapctl is-connected ${PLUG}
  then
    echo "WARNING: ${PLUG} interface not connected! Please run: /snap/${SNAP_INSTANCE_NAME}/current/bin/setup.sh"
  fi
done

if ! command -v inotifywait > /dev/null
then
    echo "ERROR: inotifywait could not be found, mir-kiosk-snap-launch expects:"
    echo " . . :     stage-packages:"
    echo " . . :        - inotify-tools"
    exit 1
fi

wait_for()
{
  until
    inotifywait --event create "$(dirname "$1")"&
    inotify_pid=$!
    [ -O "$1" ]
  do
    wait "${inotify_pid}"
  done
  kill "${inotify_pid}"
}

real_xdg_runtime_dir=$(dirname "${XDG_RUNTIME_DIR}")
real_wayland=${real_xdg_runtime_dir}/${WAYLAND_DISPLAY:-wayland-0}

# On core systems may need to wait for real XDG_RUNTIME_DIR
wait_for "${real_xdg_runtime_dir}"
wait_for "${real_wayland}"

mkdir -p "$XDG_RUNTIME_DIR" -m 700
ln -sf "${real_wayland}" "$XDG_RUNTIME_DIR"
ln -sf "${real_wayland}.lock" "$XDG_RUNTIME_DIR"
unset DISPLAY

exec "$@"

This creates a link from the snap’s $XDG_RUNTIME_DIR to the real one in the user’s $XDG_RUNTIME_DIR.

This “dance” (by design) works equally well on Ubuntu Core, running as a daemon and on Classic systems running in a user session.

The “daemon dance”

There is a difference between the way that snaps are run on Ubuntu Core and on Classic systems. On Ubuntu Core, snaps are expected to start automatically as daemons, on a Classic system they are started by the user.

To accommodate this I’ve introduced a daemon snap option, and on installation I set this according to the type of system. (Like other options this can then be changed using snap set.)

This option is used by the configure hook (another script):

#!/bin/bash
set -euo pipefail

daemon=$(snapctl get daemon)
case "$daemon" in
  true)
    # start the daemon
    if snapctl services "$SNAP_INSTANCE_NAME" | grep -q inactive; then
      snapctl start --enable "$SNAP_INSTANCE_NAME" 2>&1 || true
    fi
    ;;
  false)
    # stop the daemon
        snapctl stop --disable "$SNAP_INSTANCE_NAME" 2>&1 || true
    ;;
  *)
    echo "ERROR: Set 'daemon' to one of true|false"
    exit 1
    ;;
esac

The effect of this is that the snap will be disabled when daemon=false and enabled when daemon=true. This default is set in the install and post-refresh hooks.

The end result

If you build a snap based on the iot-example-graphical-snap examples it will have all the basics needed to work with Ubuntu Frame. (It will probably also run on a Wayland desktop, but that isn’t the point.)

Here some examples that use these techniques:

1 Like

Other stuff

For completeness, there are a few other things you need in your snap to use Wayland.

Plugs

In addition to any other interfaces needed by your application you need to list the following:

plugs:
  wayland:
  opengl:

Parts

Your application might need some additional components to use Wayland. For example, SDL2 applications (such as scummvm) need the following libraries:

parts:
  misc:
    plugin: nil
    stage-packages:
      - libwayland-client0
      - libwayland-egl1-mesa
      - libglu1-mesa
Mesa

This is subject to change as there are plans to allow the host system to provide the graphics stack. But at present you need to include and configure the Mesa graphics stack:

parts:
  mesa:
    plugin: nil
    stage-packages:
      - libgl1-mesa-dri

environment:
  # Prep EGL
  __EGL_VENDOR_LIBRARY_DIRS: $SNAP/etc/glvnd/egl_vendor.d:$SNAP/usr/share/glvnd/egl_vendor.d
  LIBGL_DRIVERS_PATH: ${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/dri
  LIBVA_DRIVERS_PATH: ${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/dri
  ...

Here’s another example: the packaging files for neverputt as a kiosk snap. This should be a good example to follow for any SDL based app.

The main part of this is the snapcraft.yaml:

name: mir-kiosk-neverputt
adopt-info: neverputt
summary: neverputt packaged as a mir-kiosk snap
description: neverputt packaged as a mir-kiosk snap
confinement: strict
grade: stable
base: core18
license: GPL-2.0

environment:
  # ${SNAPCRAFT_ARCH_TRIPLET} doesn't play nice with layout
  LD_LIBRARY_PATH: ${LD_LIBRARY_PATH}:${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/pulseaudio
  # Prep EGL
  __EGL_VENDOR_LIBRARY_DIRS: $SNAP/etc/glvnd/egl_vendor.d:$SNAP/usr/share/glvnd/egl_vendor.d
  LIBGL_DRIVERS_PATH: ${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/dri
  LIBVA_DRIVERS_PATH: ${SNAP}/usr/lib/${SNAPCRAFT_ARCH_TRIPLET}/dri
  # Prep SDL
  SDL_VIDEODRIVER: wayland

layout:
  /usr/share:
    bind: $SNAP/usr/share
  /usr/games:
    bind: $SNAP/usr/games

plugs:
  wayland:
  opengl:
  pulseaudio:
  alsa:
  hardware-observe: # This allows some UDEV access neverputt wants

apps:
  daemon:
    command: run-daemon wayland-launch neverputt
    daemon: simple
    restart-condition: always
    environment:
      # Prep PulseAudio
      PULSE_SYSTEM: 1
      PULSE_RUNTIME_PATH: /var/run/pulse

  mir-kiosk-neverputt:
    command: wayland-launch neverputt
    desktop: usr/share/applications/neverputt.desktop
    environment:
      # Prep PulseAudio
      PULSE_SERVER: unix:$XDG_RUNTIME_DIR/../pulse/native

parts:
  neverputt:
    plugin: nil
    override-pull: |
      snapcraftctl pull
      snapcraftctl set-version `LANG=C apt-cache policy neverputt | sed -rne 's/^\s+Candidate:\s+([^-]*)-.+$/\1/p'`
    stage-packages:
      - neverputt

  config:
    plugin: dump
    source: config

  mir-kiosk-snap-launch:
    plugin: dump
    source: https://github.com/MirServer/mir-kiosk-snap-launch.git
    override-build:  $SNAPCRAFT_PART_BUILD/build-with-plugs.sh opengl pulseaudio alsa wayland hardware-observe

  sdl2:
    plugin: nil
    stage-packages:
      - libsdl2-2.0-0
      - libsdl2-image-2.0-0
      - libsdl2-mixer-2.0-0
      - libsdl2-net-2.0-0

  mesa:
    plugin: nil
    stage-packages:
      - libgl1-mesa-dri
      - libwayland-egl1-mesa
      - libglu1-mesa

  wayland:
    plugin: nil
    stage-packages:
      - libwayland-client0

Things to note here are:

  1. The hardware-observe interface - although mouse & keyboard input should be access via Wayland, neverputt also accesses udev directly.
  2. The config stanza. This is where neverputt looks for configuration, and I use this to set “fullscreen” on.

The rest is simply following the template described above and documentation of the package.

Get it from the Snap Store


PS

One thing to note with SDL applications they don’t “play nice” with GNOME Wayland: Wayland session overlaid by window when fullscreen SDL apps exits

1 Like