Software update controlled by an application¶
Overview¶
In this tutorial, we'll write a small application that runs on a device, downloads new packages and installs them.
What you'll learn:
- How to create a Yocto layer on top of Welma and integrate your app
- How to use Welma's software update D-Bus API
- How to manage installation and roll back automatically
What you'll need:
-
A Raspberry Pi 4B device
-
A host computer with a Linux-based operating system;
-
The device is reachable from the host through an IP network by SSH, using the name
device-under-test.
Step 1: initial setup¶
Create your working directories on your workstation:
ROOT=... # your root working directory
mkdir $ROOT/build # your Yocto build directory
mkdir $ROOT/sources # your Yocto layers
mkdir $ROOT/www # your packages served through HTTP
Download Welma into $ROOT/sources:
cd $ROOT/sources
MACHINE=raspberrypi4-64-welma
WELMA_REF=scarthgap-next
WELMA_GIT_NAMESPACE=witekio/rnd/theembeddedkit/welma
git clone git@gitlab.com:$WELMA_GIT_NAMESPACE/welma-manifest.git
welma-manifest/setup-download welma-manifest/$WELMA_REF/manifest-$MACHINE.txt
Set up your Yocto build directory in $ROOT/build.
cd $ROOT
source sources/meta-welma/setup/setup-build-env \
sources/meta-welma-raspberrypi/conf/templates/$MACHINE
If you did not fuse the PBKH on your device, do not try to do it now, but be
sure to add WELMA_SECURE_BOOT="0" in your conf/local.conf.
Build the "dev" image:
Install the image on you device:
-
Insert the SD card in your workstation.
-
Copy the Welma image to the SD card (be careful about the destination
/dev/mmcblk0, your milage may vary):
gunzip --stdout \
tmp/deploy/images/$MACHINE/welma-image-minimal-dev-$MACHINE.wic.gz |
sudo dd of="/dev/mmcblk0" bs=10M
sync
Remove the SD card from your workstation, and insert it into your device. Do the cabling so as to get console access and SSH access to your device. Then start your device.
You should now see the device booting on the console, something like this:
0.51 RPi: BOOTSYS release VERSION:69471177 DATE: 2025/05/08 TIME: 16:21:35
...
2.53 RPi: BOOTLOADER release VERSION:69471177 DATE: 2025/05/08 TIME: 16:21:35
...
U-Boot 2024.01 (Jan 08 2024 - 15:37:48 +0000)
...
Starting kernel ...
...
[ 0.000000] Linux version 6.6.63-v8 (oe-user@oe-host) (aarch64-poky-linux-gcc (GCC) 13.4.0, GNU ld (GNU Binutils) 2.42.0.20240723) #1 SMP PREEMPT Fri Dec 6 10:10:05 UTC 2024
...
Welma by Witekio 1.6.1 raspberrypi4-64-welma ttyS0
raspberrypi4-64-welma login:
Step 2: create a Yocto layer¶
Create a dedicated directory $ROOT/sources/meta-app, in which you create
the following:
# Add layer to BBPATH
BBPATH .= ":${LAYERDIR}"
# Add recipes to BBFILES
BBFILES += "${LAYERDIR}/recipes-*/*/*.bb \
${LAYERDIR}/recipes-*/*/*.bbappend"
BBFILE_COLLECTIONS += "app-layer"
BBFILE_PATTERN_app-layer = "^${LAYERDIR}/"
BBFILE_PRIORITY_app-layer = "1000"
LAYERVERSION_app-layer = "1"
LAYERSERIES_COMPAT_app-layer = "scarthgap"
# Enable gobject introspection, needed by pydbus
DISTRO_FEATURES:append = " gobject-introspection-data"
IMAGE_INSTALL:append = " autoupdate systemd-user-appconf"
This file conf/layer.conf is mandatory for Yocto layers. It collects all
recipes and bbappends (BBFILES). It names the layer (BBFILE_COLLECTIONS).
We set a high priority (BBFILE_PRIORITY_app-layer), as we want this
project specific layer to take precedence over all other generic layers.
We also integrate our autoupdate app in the default image (IMAGE_INSTALL).
Step 3: add your application¶
We're now creating a minimal application, called autoupdate, that is in
charge of periodically polling an HTTP server, fetching new versions of appro
and installing them.
This application is made of a simple program in Python, located in
/app/bin/autoupdate (in the appro partition).
In meta-app, create the following files:
recipes-app/autoupdate/autoupdate.bb
recipes-app/autoupdate/files/autoupdate.service
recipes-app/autoupdate/files/autoupdate
recipes-core/systemd/systemd-user-appconf.bb
recipes-core/systemd/files/01-unit-reboot-on-exit.conf
Write the program itself, as follows:
#!/usr/bin/env python3
import os
import pydbus
import sys
import time
import urllib.request
DBUS_NAME = 'com.witekio.update1'
DBUS_OBJECT = '/com/witekio/update1'
DBUS_INTERFACE = 'com.witekio.update1.Manager'
AUTOUPDATE_INSTALLED = "/var/lib/autoupdate/installed" # used to keep track of which version got installed
AUTOUPDATE_PENDING = "/var/lib/autoupdate/pending" # will be moved to _INSTALLED after confirmation
AUTOUPDATE_ACTIVE_PARTITIONS = "/var/lib/autoupdate/latest-active-partitions" # used to detect rollback after reboot
def dbus_init():
bus = pydbus.SystemBus()
obj = bus.get(DBUS_NAME, DBUS_OBJECT)
itf = obj[DBUS_INTERFACE]
return obj, itf
def dbus_install(filename):
DBUS_INTERFACE.BeginUpdate()
try:
DBUS_INTERFACE.InstallLocalFile(filename)
DBUS_INTERFACE.SubmitUpdate()
except Exception:
DBUS_INTERFACE.AbortUpdate()
def wait_for_network(url):
while True:
try:
print(f"wait_for_network: GET {url}")
urllib.request.urlopen(f"{url}").read() # read() leads to closing the socket
break
except Exception:
# Retry in 5 s
time.sleep(5)
def get_active_partitions():
"""Get a determinist footprint of active partitions"""
dir_active = "/dev/disk/partitions/active"
partitions = [os.readlink(f"{dir_active}/{f}") for f in os.listdir(dir_active)]
partitions.sort()
return ' '.join(partitions)
def complete_pending_install(url):
"""Complete a pending installation and report to 'url'"""
if os.path.exists(AUTOUPDATE_PENDING):
# Let's find out if the update was successful or if we rolled back
with open(AUTOUPDATE_ACTIVE_PARTITIONS) as fd:
if get_active_partitions().strip() == fd.read().strip():
# No change. That's a rollback.
urllib.request.urlopen(f"{url}/update/rollback", data=b'') # data= implies method POST
os.unlink(AUTOUPDATE_PENDING)
else:
# Partitions changed. That means a successful update.
urllib.request.urlopen(f"{url}/update/ok", data=b'') # data= implies method POST
os.rename(AUTOUPDATE_PENDING, AUTOUPDATE_INSTALLED)
# Main
host = sys.argv[1]
port = sys.argv[2]
URL_BASE = f"http://{host}:{port}"
DBUS_OBJECT, DBUS_INTERFACE = dbus_init()
wait_for_network(URL_BASE)
if DBUS_OBJECT.State == 'under-test':
DBUS_INTERFACE.ConfirmUpdate()
complete_pending_install(URL_BASE)
while True:
try:
# Get latest revision to be installed
print(f"GET {URL_BASE}/applicable")
applicable = urllib.request.urlopen(f"{URL_BASE}/applicable").read()
applicable = applicable.decode('utf-8').strip()
try:
# Get revision of locally installed package
with open(AUTOUPDATE_INSTALLED) as fd:
installed = fd.read().strip()
except Exception:
installed = None
print(f"installed={installed}, applicable={applicable}")
if installed is None or installed != applicable:
# A new package is available. Download and install it.
print(f"GET {URL_BASE}/{applicable}")
urllib.request.urlretrieve(f"{URL_BASE}/{applicable}", "/tmp/autoupdate.pkg")
# Call install via d-bus
dbus_install("/tmp/autoupdate.pkg")
# Save active partitions
with open(AUTOUPDATE_ACTIVE_PARTITIONS, 'w') as fd:
fd.write(get_active_partitions()+"\n")
# Save the revision that we installed
with open(AUTOUPDATE_PENDING, 'w') as fd:
fd.write(f"{applicable}\n")
# Exit normally. The systemd config should trigger a reboot.
break
except Exception as e:
print(f"Failed: {e}")
time.sleep(20)
This autoupdate program, written in Python implements a minimalist algorithm:
- When it starts, it waits until it can reach a remote HTTP server
(
wait_for_network()); - Then it retrieves the applicable revision of package that should get
installed (
/applicable); - If it differs from what is installed, then it initiates an installation
and exit. Systemd is configured to reboot (see
autoupdate.serviceand01-unit-reboot-on-exit.conf); - After reboot, it waits until it can reach a remote HTTP server and confirms the update procedure.
Notes:
- We're using here a very simple scheme for the revision of a package: the revision is the filename.
- If the update procedure is not confirmed after 2 minutes, Welma triggers a reboot and rolls back the system.
- The
autoupdateprogram keeps track of active partitions before and after reboot in order to detect if a rollback occurred and report accordingly to the HTTP server. - The
autoupdateprogram keeps track of which package got installed (in/var/lib/autoupdate/installed), in order not to reinstall always the same package in an infinite loop. - It relies on the
pydbuslibrary for calling D-Bus services. - It is a good practice not to use any
.pyextension for the filename, as it can happen that we later want to code this program in another language without changing its name (and without having a filename extension that is not consistent with is contents).
We now need a Yocto recipe to have the program installed in our image:
SUMMARY = "A minimal autoupdater"
LICENSE = "CLOSED"
SRC_URI = "\
file://autoupdate \
file://autoupdate.service \
"
inherit systemd-user useradd
SYSTEMD_USER_AUTO_ENABLE = "enable"
SYSTEMD_USER_SERVICE:${PN} = "autoupdate.service"
SYSTEMD_USER_PACKAGES = "${PN}"
SYSTEMD_USERS = "user"
USERADD_PACKAGES = "${PN}"
USERADD_PARAM:${PN} = "user"
prefix = "/app"
do_install() {
install -D -m 0644 ${WORKDIR}/autoupdate.service ${D}/${systemd_user_unitdir}/autoupdate.service
install -D -m 0755 ${WORKDIR}/autoupdate ${D}/app/bin/autoupdate
install -d --owner user --group user ${D}/var/lib/autoupdate
}
FILES:${PN} += "/app"
RDEPENDS:${PN} = "welma-dm python3-pydbus"
This recipe autoupdate.bb installs the autoupdate program and has it
started automatically at boot (autoupdate.service), as a systemd user
(unprivileged) service. For that, it leverages Welma's bbclass systemd-user
(configured through SYSTEMD_USER_ variables).
It also creates /var/lib/autoupdate, which will be used by the autoupdate
program to store its files.
Let's write a systemd service description to have the app automatically started at boot time:
[Unit]
Description=Auto-update app
# When the unit stops and enters a failed state or inactive state,
# have the manager exit
SuccessAction=exit
[Service]
ExecStart=/app/bin/autoupdate 192.168.1.35 8011
Environment=PYTHONUNBUFFERED=non-empty
StandardInput=null
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
This file autoupdate.service starts the autoupdate program at boot time.
You'll need to figure out the IP address of your workstation and replace
192.168.1.35 by yours. The parameter SuccessAction=exit is a way to have
the systemd user instance stop when the program stops, and in conjunction with
01-unit-reboot-on-exit.conf below, it leads to a whole system reboot.
The parameter PYTHONUNBUFFERED= is set to prevent Python from buffering what
gets printed by the program autoupdate, so that we can see the messages in
the journal immediately when they are printed.
We need a little more systemd configuration to have the system reboot when
autoupdate exits:
SUMMARY = "systemd user configuration"
DESCRIPTION = "Reboot system when systemd user instance exits"
LICENSE = "CLOSED"
SRC_URI = "file://01-unit-reboot-on-exit.conf"
FILES:${PN} = "${systemd_unitdir}"
do_install:append() {
install -D -m 0644 ${WORKDIR}/01-unit-reboot-on-exit.conf \
${D}/${systemd_unitdir}/system/user@2000.service.d/01-unit-reboot-on-exit.conf
}
This systemd-user-appconf.bb recipe places a systemd configuration drop-in
for the systemd user instance of user 2000. That is the numeric
identifier of the user named user that is running our autoupdate program.
[Unit]
# When the unit stops and enters a failed state or inactive state, reboot
# following the normal shutdown procedure
FailureAction=reboot
SuccessAction=reboot
This 01-unit-reboot-on-exit.conf drop-in will have the systemd system
instance reboot when the systemd user instance exits.
Add meta-app in bblayers and build:
$ cat << EOF >> conf/bblayers.conf
BBLAYERS =+ "$ROOT/sources/meta-app"
EOF
$ bitbake welma-image-minimal-dev
Install the resulting image
tmp/deploy/images/$MACHINE/welma-image-minimal-dev-$MACHINE.wic.gz onto your
device, boot it, then connect via SSH and read the journal:
# journalctl -f
Jan 29 17:07:52 raspberrypi4-64-welma autoupdate[729]: GET http://192.168.1.35:8011/applicable
Jan 29 17:07:52 raspberrypi4-64-welma autoupdate[729]: Failed: <urlopen error [Errno 111] Connection refused>
Jan 29 17:08:12 raspberrypi4-64-welma autoupdate[729]: GET http://192.168.1.35:8011/applicable
Jan 29 17:08:12 raspberrypi4-64-welma autoupdate[729]: Failed: <urlopen error [Errno 111] Connection refused>
...
Step 4: serve your package¶
We'll host our packages in directory $ROOT/www.
Create a very simple server in Python, that serves files of the current directory and reports POST requests. It is not secure and should not be used for production.
#!/usr/bin/env python3
#
# Usage: python3 server.py PORT
import http.server
import socketserver
import sys
class CustomHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
super().do_GET()
def do_POST(self):
self.send_response(http.HTTPStatus.OK)
self.end_headers()
socketserver.TCPServer.allow_reuse_address = True
TCP_PORT = int(sys.argv[1])
with socketserver.TCPServer(("", TCP_PORT), CustomHandler) as httpd:
print(f"Serving HTTP on port {TCP_PORT} ...")
httpd.serve_forever()
Then copy your appro package to www:
$ cp build/tmp/deploy/images/raspberrypi4-64-welma/welma-image-minimal-dev-raspberrypi4-64-welma.appro.swu \
$ROOT/www/appro-1.1.swu
$ echo appro-1.1.swu > $ROOT/www/applicable
Finally, start serving the files:
Here, be sure to use the same TCP port as specified in autoupdate.service
above. You should finally see:
192.168.1.25 - - [29/Jan/2026 17:49:59] "GET /applicable HTTP/1.1" 200 -
192.168.1.25 - - [29/Jan/2026 17:49:59] "GET /appro-1.1.swu HTTP/1.1" 200 -
192.168.1.25 - - [29/Jan/2026 17:52:12] "GET / HTTP/1.1" 200 -
192.168.1.25 - - [29/Jan/2026 17:52:12] "POST /update/ok HTTP/1.1" 200 -
192.168.1.25 - - [29/Jan/2026 17:52:12] "GET /applicable HTTP/1.1" 200 -
192.168.1.25 - - [29/Jan/2026 17:52:32] "GET /applicable HTTP/1.1" 200 -
...
The IP address 192.168.1.25 is the one of the device and may vary.
Let's dissect these messages. All are requests or notifications coming from
autoupdate running on the device:
GET /applicable: this is the request to get which the applicable version is.GET /appro-1.1.swu: this is the request to download the packageappro-1.1.swu.GET /: this occurs more than 1 min after the previous one. The device is checking if the HTTP server is reachable (rememberwait_for_network()).POST /update/ok: this is the report that the installation procedure is complete and sucessful.GET /applicable: These are the periodic polling to check if a new version is available.
Summary¶
You learned how to create a simple Yocto layer for your application, and how to use Welma's software update mechanism.
You can further play and test installing a package from the previous tutorial,
that does not contain the autoupdate program, leading to the fact that after
reboot the installation procedure will not be confirmed and a rollback will
be triggered.
Links to the mentioned files:
- meta-app/conf/layer.conf
- meta-app/recipes-app/autoupdate/autoupdate.bb
- meta-app/recipes-app/autoupdate/files/autoupdate
- meta-app/recipes-app/autoupdate/files/autoupdate.service
- meta-app/recipes-core/systemd/files/01-unit-reboot-on-exit.conf
- meta-app/recipes-core/systemd/systemd-user-appconf.bb
- www/http-server