Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Using raptor with Zephyr

Zephyr’s mainline hawkBit client (subsys/mgmt/hawkbit) works against raptor’s DDI API unchanged. This page is the minimal wiring, plus the two footguns worth knowing before you flash a fleet.

Kconfig symbols below are from Zephyr’s subsys/mgmt/hawkbit/Kconfig; the compatibility matrix at the end records what the client actually exercises.

Minimal configuration

Device side (prj.conf):

CONFIG_HAWKBIT=y
CONFIG_HAWKBIT_SERVER="ota.example.com"
CONFIG_HAWKBIT_PORT=8088
CONFIG_HAWKBIT_TENANT="DEFAULT"
CONFIG_HAWKBIT_POLL_INTERVAL=5          # minutes; 1..43200

# Pick one auth mode (see below)
CONFIG_HAWKBIT_DDI_GATEWAY_SECURITY=y
CONFIG_HAWKBIT_DDI_SECURITY_TOKEN="shared-registration-secret"

# If raptor is behind TLS
# CONFIG_HAWKBIT_USE_TLS=y

If CONFIG_HAWKBIT_SERVER is a hostname rather than an IP, the device also needs CONFIG_DNS_RESOLVER=y.

Server side (raptor.toml):

url = "http://ota.example.com:8088"

[ddi]
gateway_token = "shared-registration-secret"
polling_interval = "00:05:00"

Keep polling_interval and CONFIG_HAWKBIT_POLL_INTERVAL consistent — raptor advertises its value as config.polling.sleep (HH:MM:SS), which the client reads and honours, so the Kconfig value is really just the pre-first-poll default.

Choosing an auth mode

The client sends exactly one of two headers, selected at build time:

Kconfig choiceHeader sentraptor side
CONFIG_HAWKBIT_DDI_GATEWAY_SECURITY=yAuthorization: GatewayToken <token>[ddi] gateway_token = "<token>"
CONFIG_HAWKBIT_DDI_TARGET_SECURITY=yAuthorization: TargetToken <token>the target’s own securityToken

Either way the token value goes in CONFIG_HAWKBIT_DDI_SECURITY_TOKEN.

Gateway token is one shared secret for the whole fleet and permits auto-registration: a device that polls with it is created on first contact. Good for bringing a fleet up, weaker blast radius if extracted from one device.

Target token is per-device, so the target must exist in raptor first, with a token you provision into the firmware:

curl -u admin:pw -X POST localhost:8088/rest/v1/targets \
  -H 'Content-Type: application/json' \
  -d '[{"controllerId": "device-42", "securityToken": "per-device-secret"}]'

See Authentication for the full picture. [ddi] anonymous = true disables DDI auth entirely — convenient for a bring-up on a lab network, never in production.

Footgun 1: the tenant must match

Zephyr’s CONFIG_HAWKBIT_TENANT defaults to "default". raptor answers to exactly one tenant, set by the tenant config key (default DEFAULT, matched case-insensitively, so the Zephyr default just works out of the box) — any other segment gets 404 on every DDI request. If your fleet is genuinely configured with a non-default tenant name, set tenant in raptor.toml to match rather than reconfiguring every device.

Footgun 2: don’t enable the confirmation flow

The client parses exactly three _links keys from the base poll — deploymentBase, configData, cancelAction. It has no confirmationBase handling at all.

So with [ddi] confirmation_flow = true, a waiting target is offered only confirmationBase, the device sees no link it understands, and it polls forever without installing and without reporting an error. If you need the flow for other clients, keep Zephyr devices out of it with:

[ddi]
confirmation_flow = true
auto_confirm_default = true    # new targets auto-confirm

See Confirmation Flow.

Compatibility matrix

What the mainline Zephyr client uses, and raptor’s status for each. “Verified” means covered by raptor’s integration tests, including a JSON-contract test (zephyr_client_json_contract) pinning the exact response shape the client’s strict JSON descriptors require.

DDI featureZephyr clientraptor
Base poll config.polling.sleep (HH:MM:SS)reads and honours✅ verified
_links.deploymentBaseparsed✅ verified
_links.configDataparsed; uploads whenever present✅ verified — advertised only when attributes are wanted, so devices don’t re-upload every poll
_links.cancelActionparsed✅ verified
_links.confirmationBasenot parsed⚠️ raptor supports it; do not enable for Zephyr (footgun 2)
configData PUT, mode: "merge"sends on every poll carrying the link✅ verified
deploymentBase chunks/artifactsparsed✅ verified
Artifact hashes.sha256verified against flashed image✅ verified
Artifact _links.download-httpthe link it downloads from✅ verified — see download-http vs download
Range: resume (CONFIG_HAWKBIT_SAVE_PROGRESS)sends bytes=N-✅ verified — 206 + Content-Range
Feedback execution / resultclosed, proceeding, canceled, scheduled, rejected, resumed, none / success, failure, none✅ verified
Authorization: TargetToken / GatewayTokeneither, chosen at build time✅ verified
Multi-tenancyCONFIG_HAWKBIT_TENANT⚠️ single-tenant, must match configured tenant (footgun 1)

Sharing a server with Linux devices

Zephyr devices and Linux devices running SWUpdate can poll the same raptor. One client limit makes the separation matter: Zephyr’s client rejects any deployment that isn’t exactly one chunk with exactly one artifact (hawkbit.c, “Expecting 1 chunk” / “Expecting 1 artifact”). A Linux set with two modules, or a zchunk delta module holding both a .swu and a .zck, is refused by a Zephyr device before it downloads anything.

raptor needs no special handling for this. Target types already keep the two apart: a typed target accepts only distribution sets of a compatible type, and direct assignment, auto-assigning target filters and rollouts all enforce that. Give MCU sets their own type built on the seeded application module type, and type every device. It must be application: raptor serves that module type as DDI chunk part bApp, the only part Zephyr’s client accepts (“Only part ‘bApp’ is supported”). The os and firmware module types are served as part os and refused.

# A distribution-set type that needs one application module (look up its id first)
curl -u admin:pw localhost:8088/rest/v1/softwaremoduletypes
curl -u admin:pw -H 'Content-Type: application/json' -X POST \
  localhost:8088/rest/v1/distributionsettypes \
  -d '[{"key":"mcu","name":"MCU firmware","mandatorymodules":[{"id":<application-id>}]}]'

# Target types: Zephyr devices take only mcu, Linux devices only os/os_app
curl -u admin:pw -H 'Content-Type: application/json' -X POST \
  localhost:8088/rest/v1/targettypes \
  -d '[{"name":"zephyr","compatibledistributionsettypes":[{"id":<mcu-id>}]},
       {"name":"linux","compatibledistributionsettypes":[{"id":<os-id>},{"id":<os_app-id>}]}]'

raptorctl target type set dev-042 zephyr
raptorctl publish zephyr.signed.bin --version 1.4.1 --module-type application --ds-type mcu

An untyped target accepts any set, so leaving a device untyped removes the protection. Typing doesn’t limit a module to one artifact either, so don’t upload anything else into an MCU module.

Update size: no delta, but compression

Delta updates aren’t available for Zephyr application images today. This was checked in October 2026 against Zephyr, upstream MCUboot, and Nordic’s sdk-mcuboot/sdk-nrf main branches:

  • The Zephyr hawkBit client always downloads the whole image into slot 1 and lets MCUboot swap it in. The only Range it sends is bytes=N-, to resume a download (CONFIG_HAWKBIT_SAVE_PROGRESS), so the multi-range serving that zchunk relies on goes unused.
  • Neither MCUboot nor the nRF Connect SDK patches an application image from a diff. NCS’s delta DFU (dfu_target_modem_delta) is for nRF91 modem firmware only, and the modem applies it itself.

The nearest option is compressed images: MCUboot in NCS (SB_CONFIG_MCUBOOT_COMPRESSED_IMAGE_SUPPORT, LZMA2 with an ARM thumb filter) decompresses slot 1 into slot 0 at boot. The download and slot 1 come out at roughly 70% of the image size. MCUboot has to run in overwrite-only mode (SB_CONFIG_MCUBOOT_MODE_OVERWRITE_ONLY), so a bad image can’t be reverted. The client’s own check is only that the artifact fits in slot 1. To raptor and the hawkBit client the compressed image is just a smaller artifact, so nothing changes on the server. Upstream MCUboot has the same BOOT_DECOMPRESSION code path, but its decompressor comes from NCS (nrf_compress).

A real delta for MCUs would be a binary diff built against one specific base version, published as its own set and aimed with a target filter on a firmware-version attribute. raptor can already serve that. What doesn’t exist yet is the device side: a hawkBit client that writes through a patcher instead of flash_img.

Verifying against a real device

raptor’s test suite drives its DDI API with the Rust hawkbit crate and the JSON-contract test above — not with the Zephyr client itself, which needs hardware or QEMU. For an end-to-end check, build Zephyr’s samples/subsys/mgmt/hawkbit sample against a raptor instance and watch the server log with RUST_LOG=raptor=debug,tower_http=debug: a successful cycle is a base poll, a configData PUT, a deploymentBase GET, artifact GETs, then feedback with execution: "closed" — which also logs update finished at the default level.