RDFM MCUmgr Device Client¶
Introduction¶
The RDFM MCUmgr Device Client (rdfm-mcumgr-client
) allows for integrating an embedded device running ZephyrRTOS with the RDFM server via its MCUmgr SMP server implementation.
Currently, only the update functionality is implemented with support for serial, UDP and BLE transports.
rdfm-mcumgr-client
runs on a proxy device that’s connected to the targets via one of the supported transports that handles the process of checking for updates, fetching update artifacts and pushing update images down to correct targets.
Getting started¶
In order to properly function, both the Zephyr application and the rdfm-mcumgr-client
have to be correctly configured in order for the update functionality to work.
Specifically:
Zephyr applications must be built with MCUmgr support, with any transport method of your choice and with image management and reboot command groups enabled.
The device running Zephyr must be connected to a proxy device running
rdfm-mcumgr-client
as the updates are coming from it.For reliable updates, the SMP server must be running alongside your application and be accessible at all times.
Building client from source¶
Requirements¶
C compiler
Go compiler (1.22+)
liblzma-dev and libssl-dev packages
Steps¶
To install the proxy client from source, first clone the repository and build the binary:
git clone https://github.com/antmicro/rdfm.git
cd rdfm/devices/mcumgr-client/
make
Then run the install command:
make install
Setting up target device¶
Setting up the bootloader¶
To allow rollbacks and update verification, the MCUboot bootloader is used.
Images uploaded by rdfm-mcumgr-client
are written to a secondary flash partition, while leaving the primary (currently running) image intact.
During update, the images are swapped by the bootloader.
If the update was successful, the new image is permanently set as the primary one, otherwise the images are swapped back to restore the previous version.
For more details on MCUboot, you can read the official guide from MCUboot’s website.
Generating image signing key¶
In order to enable updates, MCUboot requires all images to be signed. During update, the bootloader will first validate the image using this key.
MCUboot provides imgtool.py
image tool script which can be used to generate appropriate signing key.
Below are the steps needed to generate a new key using this tool:
Install additional packages required by the tool (replace ~/zephyrproject
with path to your Zephyr workspace):
cd ~/zephyrproject/bootloader/mcuboot
pip3 install --user -r ./scripts/requirements.txt
Generate new key:
cd ~/zephyrproject/bootloader/mcuboot/scripts
./imgtool.py keygen -k <filename.pem> -t <key-type>
MCUboot currently supports rsa-2048
, rsa-3072
, ecdsa-p256
or ed25519
key types.
For more details on the image tool, please refer to its official documentation.
Building the bootloader¶
Besides the signing key, MCUboot also requires that the target board has specific flash partitions defined in its devicetree. These partitions are:
boot_partition
: for MCUboot itselfslot0_partition
: the priamry slot of image 0slot1_partition
: the secondary slot of image 0
If you choose the swap-using-scratch update algorithm, one more partition has to be defined:
scratch_partition
: the scratch slot
You can check whether your board has those partitions predefined by looking at its devicetree file (boards/<arch>/<board>/<board>.dts
).
Look for fixed-partitions
compatible entry. If your default board configuration doesn’t specify those partitions (or you would like to modify them),
you can either modify the devicetree file directly or use devicetree overlays.
Sample overlay file for the stm32f746g_disco
board:
#include <mem.h>
/delete-node/ &quadspi;
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 DT_SIZE_K(64)>;
};
slot0_partition: partition@40000 {
label = "image-0";
reg = <0x00040000 DT_SIZE_K(256)>;
};
slot1_partition: partition@80000 {
label = "image-1";
reg = <0x00080000 DT_SIZE_K(256)>;
};
scratch_partition: partition@c0000 {
label = "scratch";
reg = <0x000c0000 DT_SIZE_K(256)>;
};
};
};
/ {
aliases {
/delete-property/ spi-flash0;
};
chosen {
zephyr,flash = &flash0;
zephyr,flash-controller = &flash;
zephyr,boot-partition = &boot_partition;
zephyr,code-partition = &slot0_partition;
};
};
Note
If you do use devicetree overlay, make sure to add app.overlay
as the last overlay file
since it’s needed to correctly store the MCUboot image in boot_partition
.
Besides the devicetree, you also have to specify:
BOOT_SIGNATURE_KEY_FILE
: path to the previously generate signing keyBOOT_SIGNATURE_TYPE
: signing key type:BOOT_SIGNATURE_TYPE_RSA
andBOOT_SIGNATURE_TYPE_RSA_LEN
BOOT_SIGNATURE_TYPE_ECDSA_P256
BOOT_SIGNATURE_TYPE_ED25519
BOOT_IMAGE_UPGRADE_MODE
: the update algorithm used for swapping images in primary and secondary slots:BOOT_SWAP_USING_MOVE
BOOT_SWAP_USING_SCRATCH
For example, if you wanted to build the bootloader for the stm32f746g_disco
board with partitions defined in stm32_disco.overlay
,
using swap-using-scratch update algorithm and using rsa-2048
key.pem
signing key,
you would run (replace ~/zephyrproject
with path to your Zephyr workspace):
west build \
-d mcuboot \
-b stm32f746g_disco \
~/zephyrproject/bootloader/mcuboot/boot/zephyr \
-- \
-DDTC_OVERLAY_FILE="stm32_disco.overlay;app.overlay" \
-DCONFIG_BOOT_SIGNATURE_KEYFILE='"key.pem"' \
-DCONFIG_BOOT_SIGNATURE_TYPE_RSA=y \
-DCONFIG_BOOT_SIGNATURE_TYPE_RSA_LEN=2048 \
-DCONFIG_BOOT_SWAP_USING_SCRATCH=y
The produced image can be flashed to your device. For more details on building and using MCUboot with Zephyr, please refer to official MCUboot guide.
Setting up the Zephyr application¶
Building the image¶
To allow your application to be used with MCUmgr client, you will have to enable Zephyr’s device management subsystem.
For the client to function properly, both image management
and OS management groups need to be enabled.
You will also have to enable and configure SMP transport
(either serial, BLE or udp) that you wish to use.
To learn how to do that, you can reference Zephyr’s smp_svr
sample
which provides configuration for all of them.
You will also have set MCUBOOT_BOOTLOADER_MODE
setting to match the swapping algorithm you’ve configured for the bootloader:
MCUboot |
Zephyr |
---|---|
|
|
|
|
Important
Bluetooth specific
Bluetooth transport additionally requires you to manually start SMB Bluetooth advertising.
Refer to the main.c
and bluetooth.c
from the smp_svr
sample for details on that.
To build the smp_svr
sample
for the stm32f746g_disco
board with stm32_disco.overlay
devicetree overlay,
configured to use serial transport with swap-using-scratch update algorithm,
you would run (replace ~/zephyrproject
with path to your Zephyr workspace):
west build \
-d build \
-b stm32f746g_disco \
"~/zephyrproject/zephyr/samples/subsys/mgmt/mcumgr/smp_svr" \
-- \
-DDTC_OVERLAY_FILE="stm32_disco.overlay" \
-DEXTRA_CONF_FILE="overlay-serial.conf" \
-DCONFIG_MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH=y
For more information on the smp_svr
sample, please refer to Zephyr’s documentation.
Signing the image¶
By default MCUboot will only accept images that are properly signed with the same key as the bootloader itself.
Only BIN
and HEX
output types can be signed.
The recommended way for managing signing keys is using MCUboot’s image tool,
which is shipped together with Zephyr’s MCUboot implementation.
When signing an image, you also have to provide an image version, that’s embedded in the signed image header.
This is also the value that will be reported by the MCUmgr client as the current running software version back to the RDFM server.
Image version is specified in major.minor.revision+build
format.
Automatically¶
Zephyr build system can automatically sign the final image for you. To enable this functionality, you will have to set:
MCUBOOT_SIGNATURE_KEY_FILE
: path to the signing keyMCUBOOT_IMGTOOL_SIGN_VERSION
: version of the produced image before building your application. Here’s a modification of the build command from building the image with those settings applied:
west build \
-d build \
-b stm32f746g_disco \
"~/zephyrproject/zephyr/samples/subsys/mgmt/mcumgr/smp_svr" \
-- \
-DDTC_OVERLAY_FILE="stm32_disco.overlay" \
-DEXTRA_CONF_FILE="overlay-serial.conf" \
-DCONFIG_MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH=y \
-DCONFIG_MCUBOOT_SIGNATURE_KEY_FILE='"key.pem"' \
-DCONFIG_IMGTOOL_SIGN_VERSION='"1.2.3+4"'
Manually¶
You can also sign the produced images yourself using the image tool. Below is a sample showing how to sign previously built image:
west sign -d build -t imgtool -- --key <key-file> --version <sign-version>
Either way, the signed images will be stored next to their unsigned counterparts. They will have signed
inserted into the filename (e.g. unsigned zephyr.bin
will produce zephyr.signed.bin
signed image).
Self-confirmed updates¶
By default, MCUmgr client will try to manually confirm a new image during an update. While this works in simple cases, you might wish to run some additional test logic that should be used to determine if an update should be finalized. For example, you might want to reject an update in case one of the drivers failed to start or if the network stack is misconfigured. The client supports these kinds of use cases using self-confirming images. Rather than confirming an update by itself, the client will instead watch the primary image slot of the device to determine if an update was marked as permanent or if it was rejected. In that case, the final decision falls on the updated device.
For this feature to work correctly, you will have to modify your application to include the self-testing logic.
/*
* An example of self-test function.
* It will first check if this is a fresh update and run the testing logic.
* Based on results, it will either mark the update as permanent or reboot,
* causing MCUboot to revert to the previous version.
*
* This function should be called before the main application logic starts,
* preferably at the beginning of the `main` function.
*/
#include <zephyr/dfu/mcuboot.h>
#include <zephyr/sys/reboot.h>
void run_self_tests() {
if (!boot_is_img_confirmed()) {
bool passed;
/* Testing logic goes here */
if (!passed) {
sys_reboot(SYS_REBOOT_COLD); // (1)
return;
}
boot_write_img_confirmed(); // (2)
}
}
Tests failed - device reboots itself, returning to previous version
Tests passed - device confirms the update, marking it as permanent
Configuring MCUmgr client¶
Search locations¶
The client is configured using config.json
configuration file.
By default, the client will look for this file in:
current working directory
$HOME/.config/rdfm-mcumgr
/etc/rdfm-mcumgr
stopping at first configuration file found.
You can override this by specifying path to a different configuration file with -c/--config
flag:
rdfm-mcumgr-client --config <path-to-config>
All of the non-device specific options can also be overwritten by specifying their flag counterpart. For a full list you can run:
rdfm-mcumgr-client --help
Configuration values¶
server
- URL of the RDFM server the client should connect tokey_dir
- path (relative or absolute) to the directory where all device keys are storedupdate_interval
- interval between each update poll to RDFM server (accepts time suffixes ‘s’, ‘m’, ‘h’)retries
- (optional) how many times should an update be attempted for a device in case of an error (no value or value0
means no limit)devices
- an array containing configuration for each device the client should handlename
- display name for device, used only for loggingid
- unique device identifier used when communicating with RDFM serverdevice_type
- device type reported to RDFM server used to specify compatible artifactskey
- name of the file containing device private key in PEM format. Key should be stored inkey_dir
directory.self_confirm
- (optional) bool indicating whether the device will confirm updates by itself. False by defaultupdate_interval
- (optional) override globalupdate_interval
for this devicetransport
- specifies the transport type for the device and it’s specific options
groups
- an array containing configuration for device groupsname
- display name for group, used for loggingid
- unique group identifier used when communicating with RDFM servertype
- type reported to RDFM server to specify compatible artifactskey
- name of the file containing group private key in PEM format. Key should be stored inkey_dir
directory.update_interval
- (optional) override globalupdate_interval
for this groupmembers
- an array containing configuration for each device that’s a member of this groupname
- display name for device, used for loggingdevice
- name of target image to match from an artifactself_confirm
- (optional) bool indicating whether the device will confirm updates by itself. False by defaulttransport
- specifies the transport type for the device and its specific options
Transport specific:
type
- specific transport type for this device. Currently supported:ble
,serial
,udp
BLE transport:
device_index
- controller index to be used for connection (e.g.hci0
->0
)peer_name
- the name the target BLE device advertises. Should match withCONFIG_BT_DEVICE_NAME
Serial transport:
device
- device name used for communicating with device. OS specific (e.g."/dev/ttyUSB0"
,"/dev/tty.usbserial"
)baud
- communication speed; must match the baudrate of connected devicemtu
- Maximum Transmission Unit, maximum protocol packet size
UDP transport:
address
: IPv4 / IPv6 address and port inIP
:port
form
Device groups¶
The client supports grouping multiple Zephyr MCUboot boards to act as one complete device from management server’s perspective. While each device in a group can be running different Zephyr application, all devices are synchronized by the MCUmgr client to be running the exact same software version. Group updates are performed using zephyr group artifacts which contain update images for each member of the group and metadata on how to match image to device.
During an update, the MCUmgr client matches each image to its target member and tries to apply it. Group update is considered successful only if all members of the group went through the update process without errors. Otherwise all members are rolled back by the client to the previous version.
Example configuration¶
{
"server": "http://localhost:5000",
"key_dir": "keys",
"update_interval": "10s",
"retries": 3,
"devices": [
{
"name": "zephyr-ble",
"id": "11:11:11:11:11:11",
"dev_type": "zeph-ble",
"update_interval": "15s",
"key": "ble.key",
"transport": {
"type": "ble",
"device_index": 0,
"peer_name": "test0"
}
},
{
"name": "zephyr-serial",
"id": "22:22:22:22:22:22",
"dev_type": "zeph-ser",
"key": "serial.key",
"self_confirm": true,
"transport": {
"type": "serial",
"device": "/dev/ttyACM0",
"baud": 115200,
"mtu": 128
}
}
],
"groups": [
{
"name": "group-one",
"id": "gr1",
"type": "group1",
"key": "group1.key",
"members": [
{
"name": "udpl",
"device": "udp-left",
"transport": {
"type": "udp",
"address": "192.168.1.2:1337"
}
},
{
"name": "udpr",
"device": "udp-right",
"transport": {
"type": "udp",
"address": "192.168.1.3:1337"
}
},
{
"name": "bleh",
"device": "ble",
"self_confirm": true,
"transport": {
"type": "ble",
"device_index": 0,
"peer_name": "ble_head",
}
}
]
}
]
}
Device keys¶
Each device uses its own private key for authentication with rdfm-server
as described in device authentication.
Each key should be stored under key_dir
specified in configuration.
If the client doesn’t find corresponding device key for configured device, it will attempt to generate one itself.
The resulting key will be saved to the configured location with 0600
permissions.
Note
Device keys are different from the signing key used for signing the bootloader and application images!