From e5b14ba1b4fdf44e086503da092f9bce4a3509db Mon Sep 17 00:00:00 2001 From: reubenmiller Date: Tue, 10 Nov 2026 17:16:17 +1000 Subject: [PATCH 1/2] feat(zephyr): one call starts provisioning with the port's hooks tedge_provisioning_zephyr_init() starts the provisioning library with the hooks that are the same in every Zephyr application: - Wi-Fi: join the network, store it in Zephyr's Wi-Fi credentials store only once the device has an address, and otherwise rejoin the previously stored network, so a mistyped password cannot strand a device; - the scan; - identify, blinking the devicetree's led0 or the first pixel of its led-strip; - CONFIG_SOC as the chip. Each hook the application passes replaces the port's, for applications that manage their network themselves. The minimal sample drops its own Wi-Fi hook and uses the defaults. On an ESP32-S3-DevKitC, the sample answers info, the scan, and a network that does not exist ("could not connect"). Co-Authored-By: Claude Opus 5.5 --- ports/zephyr/CMakeLists.txt | 2 + .../include/tedge/provisioning_zephyr.h | 44 ++- ports/zephyr/samples/minimal/src/main.c | 77 +---- ports/zephyr/src/tedge_provisioning_zephyr.c | 279 ++++++++++++++++++ 4 files changed, 320 insertions(+), 82 deletions(-) create mode 100644 ports/zephyr/src/tedge_provisioning_zephyr.c diff --git a/ports/zephyr/CMakeLists.txt b/ports/zephyr/CMakeLists.txt index 9250ff0..0fe2807 100644 --- a/ports/zephyr/CMakeLists.txt +++ b/ports/zephyr/CMakeLists.txt @@ -37,6 +37,8 @@ if(CONFIG_TEDGE) zephyr_library_sources_ifdef(CONFIG_TEDGE_AUTH_C8Y_CA src/tedge_port_crypto.c) zephyr_library_sources_ifdef(CONFIG_TEDGE_PROVISIONING_SERIAL src/tedge_provisioning_console.c) + zephyr_library_sources_ifdef(CONFIG_TEDGE_PROVISIONING + src/tedge_provisioning_zephyr.c) if(CONFIG_TEDGE_PROVISIONING AND CONFIG_WIFI) zephyr_library_sources(src/tedge_provisioning_wifi.c) endif() diff --git a/ports/zephyr/include/tedge/provisioning_zephyr.h b/ports/zephyr/include/tedge/provisioning_zephyr.h index ec23ab6..b4b3f53 100644 --- a/ports/zephyr/include/tedge/provisioning_zephyr.h +++ b/ports/zephyr/include/tedge/provisioning_zephyr.h @@ -4,17 +4,34 @@ * @file * @brief Ready-made provisioning hooks for Zephyr applications. * - * Scanning for Wi-Fi networks is the same in every Zephyr application, so - * the port provides the hook (tedge_provisioning_hooks::wifi_scan). - * Joining and storing a network stays the application's, since only it - * knows how it keeps its credentials: + * Most of what the provisioning library asks of an application is the same + * in every Zephyr application, so the port provides it: + * + * - Wi-Fi: join the network, keep it in Zephyr's Wi-Fi credentials store + * (CONFIG_WIFI_CREDENTIALS) only once the device has an address, and go + * back to the previous network otherwise; + * - the scan, on the first Wi-Fi interface; + * - identify: blink the devicetree's led0, or the first pixel of its + * led-strip; + * - the chip: CONFIG_SOC. + * + * One call starts the library with them: + * + * @code + * tedge_init(NULL, &hooks); + * tedge_provisioning_zephyr_init(NULL); + * tedge_start(); + * @endcode + * + * An application that manages the network itself, or has a better light, + * passes its own hooks; each one it sets replaces the port's: * * @code - * static const struct tedge_provisioning_hooks hooks = { - * .wifi_connect = my_join_and_store, - * .wifi_scan = tedge_provisioning_wifi_scan, - * .chip = CONFIG_SOC, + * static const struct tedge_provisioning_hooks mine = { + * .wifi_connect = my_network_switch, + * .identify = my_status_led_blink, * }; + * tedge_provisioning_zephyr_init(&mine); * @endcode */ @@ -23,7 +40,16 @@ #include -/** Scan on the first Wi-Fi interface (needs CONFIG_WIFI). */ +/** + * Start the provisioning library with the port's hooks, replaced by any + * member of @p overrides that is set (NULL keeps them all). Call after + * tedge_init(). + * + * @return 0, or a negative errno. + */ +int tedge_provisioning_zephyr_init(const struct tedge_provisioning_hooks *overrides); + +/** The port's scan hook, on the first Wi-Fi interface (needs CONFIG_WIFI). */ int tedge_provisioning_wifi_scan(struct tedge_provisioning_network *out, size_t max, void *ctx); diff --git a/ports/zephyr/samples/minimal/src/main.c b/ports/zephyr/samples/minimal/src/main.c index ecaaf55..6634103 100644 --- a/ports/zephyr/samples/minimal/src/main.c +++ b/ports/zephyr/samples/minimal/src/main.c @@ -15,8 +15,10 @@ #include #if defined(CONFIG_TEDGE_PROVISIONING) +/* Setting the device up with a tool (tedge-onboard) over the console: the + * port's hooks join and store the network it is given, so the next boot + * joins it by itself. */ #include -#include #include #endif @@ -67,82 +69,11 @@ static int wifi_connect(void) } #endif -#if defined(CONFIG_TEDGE_PROVISIONING) -/* Setting the device up with a tool (tedge-onboard) over the console: the - * library hands this application the Wi-Fi network to join, and the client - * the Cumulocity tenant. The network is stored with Zephyr's Wi-Fi - * credentials, so the next boot joins it by itself. */ - -static K_SEM_DEFINE(wifi_done, 0, 1); -static int wifi_status; -static struct net_mgmt_event_callback wifi_cb; - -static void wifi_event(struct net_mgmt_event_callback *cb, uint64_t event, - struct net_if *iface) -{ - ARG_UNUSED(iface); - if (event == NET_EVENT_WIFI_CONNECT_RESULT) { - const struct wifi_status *status = cb->info; - - wifi_status = status->status; - k_sem_give(&wifi_done); - } -} - -static int provision_wifi(const char *ssid, const char *password, void *ctx) -{ - struct net_if *iface = net_if_get_first_wifi(); - enum wifi_security_type security = password[0] != '\0' - ? WIFI_SECURITY_TYPE_PSK - : WIFI_SECURITY_TYPE_NONE; - struct wifi_connect_req_params p = { - .ssid = (const uint8_t *)ssid, - .ssid_length = strlen(ssid), - .psk = (const uint8_t *)password, - .psk_length = strlen(password), - .security = security, - .channel = WIFI_CHANNEL_ANY, - .band = WIFI_FREQ_BAND_2_4_GHZ, - }; - int rc; - - ARG_UNUSED(ctx); - if (iface == NULL) { - return -ENODEV; - } - (void)net_mgmt(NET_REQUEST_WIFI_DISCONNECT, iface, NULL, 0); - k_sem_reset(&wifi_done); - rc = net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, &p, sizeof(p)); - if (rc != 0) { - return rc; - } - if (k_sem_take(&wifi_done, K_SECONDS(30)) != 0) { - return -ETIMEDOUT; - } - if (wifi_status != 0) { - return -ECONNREFUSED; - } - /* Joined: keep it, and only it, for the next boot. */ - (void)wifi_credentials_delete_all(); - return wifi_credentials_set_personal(ssid, strlen(ssid), security, NULL, 0, - password, strlen(password), 0, 0, 0); -} - -static const struct tedge_provisioning_hooks provisioning_hooks = { - .wifi_connect = provision_wifi, - .wifi_scan = tedge_provisioning_wifi_scan, - .chip = CONFIG_SOC, -}; -#endif - int main(void) { LOG_INF("tedge-zephyr %s", tedge_version()); #if defined(CONFIG_TEDGE_PROVISIONING) - net_mgmt_init_event_callback(&wifi_cb, wifi_event, - NET_EVENT_WIFI_CONNECT_RESULT); - net_mgmt_add_event_callback(&wifi_cb); if (!wifi_credentials_is_empty()) { /* Set up before: join the network it was given. */ (void)net_mgmt(NET_REQUEST_WIFI_CONNECT_STORED, @@ -161,7 +92,7 @@ int main(void) if (tedge_init(NULL, &hooks) == 0) { #if defined(CONFIG_TEDGE_PROVISIONING) - (void)tedge_provisioning_init(&provisioning_hooks); + (void)tedge_provisioning_zephyr_init(NULL); #endif (void)tedge_start(); } diff --git a/ports/zephyr/src/tedge_provisioning_zephyr.c b/ports/zephyr/src/tedge_provisioning_zephyr.c new file mode 100644 index 0000000..b1496bf --- /dev/null +++ b/ports/zephyr/src/tedge_provisioning_zephyr.c @@ -0,0 +1,279 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * Ready-made provisioning hooks for Zephyr applications + * (): + * + * - Wi-Fi: join the network on the first Wi-Fi interface, keep it in + * Zephyr's Wi-Fi credentials store only once the device has an address, + * and otherwise go back to the network it had, so a mistyped password + * cannot strand a device. + * - Identify: blink the board's led0, or the first pixel of its led-strip, + * when the devicetree names one. + * + * tedge_provisioning_zephyr_init() starts the library with these, the scan + * (tedge_provisioning_wifi.c) and the SoC's name, letting the application + * replace any of them. + */ + +#include + +#include +#include + +#if defined(CONFIG_WIFI) +#include +#include +#include +#endif +#if defined(CONFIG_WIFI_CREDENTIALS) +#include +#endif +#if DT_HAS_ALIAS(led0) && defined(CONFIG_GPIO) +#include +#define HAVE_LED0 1 +#endif +#if DT_HAS_ALIAS(led_strip) && defined(CONFIG_LED_STRIP) +#include +#define HAVE_LED_STRIP 1 +#endif + +#include +#include + +LOG_MODULE_DECLARE(tedge, CONFIG_TEDGE_LOG_LEVEL); + +/* --- Wi-Fi ---------------------------------------------------------------- */ + +#if defined(CONFIG_WIFI) && defined(CONFIG_WIFI_CREDENTIALS) + +#define JOIN_TIMEOUT K_SECONDS(30) +#define ADDRESS_WAIT_MS 15000 + +static K_SEM_DEFINE(join_done, 0, 1); +static int join_status; +static struct net_mgmt_event_callback join_cb; +static bool join_cb_added; + +static void join_event(struct net_mgmt_event_callback *cb, uint64_t event, + struct net_if *iface) +{ + ARG_UNUSED(iface); + if (event == NET_EVENT_WIFI_CONNECT_RESULT) { + const struct wifi_status *status = cb->info; + + join_status = status->status; + k_sem_give(&join_done); + } +} + +/* Join @p ssid; 0 once the device has an IPv4 address. */ +static int join(struct net_if *iface, const char *ssid, size_t ssid_len, + const char *password, size_t password_len, + enum wifi_security_type security) +{ + struct wifi_connect_req_params p = { + .ssid = (const uint8_t *)ssid, + .ssid_length = ssid_len, + .psk = (const uint8_t *)password, + .psk_length = password_len, + .security = security, + .channel = WIFI_CHANNEL_ANY, + .band = WIFI_FREQ_BAND_UNKNOWN, + .mfp = WIFI_MFP_OPTIONAL, + }; + int64_t until; + int rc; + + (void)net_mgmt(NET_REQUEST_WIFI_DISCONNECT, iface, NULL, 0); + k_sem_reset(&join_done); + rc = net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, &p, sizeof(p)); + if (rc != 0) { + return rc; + } + if (k_sem_take(&join_done, JOIN_TIMEOUT) != 0) { + return -ETIMEDOUT; + } + if (join_status != 0) { + return -ECONNREFUSED; + } + until = k_uptime_get() + ADDRESS_WAIT_MS; + while (net_if_ipv4_get_global_addr(iface, NET_ADDR_PREFERRED) == NULL) { + if (k_uptime_get() >= until) { + return -ETIMEDOUT; + } + k_msleep(250); + } + return 0; +} + +/* The first stored network, if there is one. */ +struct stored { + struct wifi_credentials_personal creds; + bool present; +}; + +static void first_ssid(void *arg, const char *ssid, size_t ssid_len) +{ + struct stored *s = arg; + + if (!s->present && + wifi_credentials_get_by_ssid_personal_struct(ssid, ssid_len, + &s->creds) == 0) { + s->present = true; + } +} + +static int wifi_connect(const char *ssid, const char *password, void *ctx) +{ + struct net_if *iface = net_if_get_first_wifi(); + enum wifi_security_type security = password[0] != '\0' + ? WIFI_SECURITY_TYPE_PSK + : WIFI_SECURITY_TYPE_NONE; + static struct stored previous; /* too big for the worker's stack */ + int rc; + + ARG_UNUSED(ctx); + if (iface == NULL) { + return -ENODEV; + } + if (!join_cb_added) { + net_mgmt_init_event_callback(&join_cb, join_event, + NET_EVENT_WIFI_CONNECT_RESULT); + net_mgmt_add_event_callback(&join_cb); + join_cb_added = true; + } + memset(&previous, 0, sizeof(previous)); + wifi_credentials_for_each_ssid(first_ssid, &previous); + + rc = join(iface, ssid, strlen(ssid), password, strlen(password), + security); + if (rc == 0) { + /* It works: keep it, and only it. */ + (void)wifi_credentials_delete_all(); + rc = wifi_credentials_set_personal(ssid, strlen(ssid), security, + NULL, 0, password, + strlen(password), 0, 0, 0); + if (rc != 0) { + LOG_ERR("provisioning: joined \"%s\" but could not store " + "it (%d)", ssid, rc); + } + } else if (previous.present) { + LOG_WRN("provisioning: could not join \"%s\" (%d); back to " + "\"%.*s\"", ssid, rc, (int)previous.creds.header.ssid_len, + previous.creds.header.ssid); + (void)join(iface, previous.creds.header.ssid, + previous.creds.header.ssid_len, + previous.creds.password, previous.creds.password_len, + previous.creds.header.type); + } + memset(&previous, 0, sizeof(previous)); + return rc; +} + +#endif /* CONFIG_WIFI && CONFIG_WIFI_CREDENTIALS */ + +/* --- Identify ------------------------------------------------------------- */ + +#if defined(HAVE_LED0) || defined(HAVE_LED_STRIP) + +#define BLINK_MS 100 +#define IDENTIFY_MS 10000 + +#if defined(HAVE_LED0) +static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios); +#endif +#if defined(HAVE_LED_STRIP) +static const struct device *const strip = DEVICE_DT_GET(DT_ALIAS(led_strip)); +#endif + +static int blinks_left; +static void blink(struct k_work *work); +static K_WORK_DELAYABLE_DEFINE(blink_work, blink); + +static void led_set(bool on) +{ +#if defined(HAVE_LED0) + if (gpio_is_ready_dt(&led)) { + (void)gpio_pin_set_dt(&led, on); + } +#endif +#if defined(HAVE_LED_STRIP) + if (device_is_ready(strip)) { + struct led_rgb px = { .r = on ? 32 : 0, .g = on ? 32 : 0, + .b = on ? 32 : 0 }; + + (void)led_strip_update_rgb(strip, &px, 1); + } +#endif +} + +static void blink(struct k_work *work) +{ + ARG_UNUSED(work); + led_set(blinks_left % 2 == 0); + if (--blinks_left > 0) { + (void)k_work_schedule(&blink_work, K_MSEC(BLINK_MS)); + } else { + led_set(false); + } +} + +static void identify(void *ctx) +{ + ARG_UNUSED(ctx); +#if defined(HAVE_LED0) + if (gpio_is_ready_dt(&led)) { + (void)gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE); + } +#endif + blinks_left = IDENTIFY_MS / BLINK_MS; + (void)k_work_reschedule(&blink_work, K_NO_WAIT); +} + +#endif /* HAVE_LED0 || HAVE_LED_STRIP */ + +/* --- Setup ---------------------------------------------------------------- */ + +int tedge_provisioning_zephyr_init(const struct tedge_provisioning_hooks *overrides) +{ + static struct tedge_provisioning_hooks hooks; + + memset(&hooks, 0, sizeof(hooks)); +#if defined(CONFIG_WIFI) && defined(CONFIG_WIFI_CREDENTIALS) + hooks.wifi_connect = wifi_connect; +#endif +#if defined(CONFIG_WIFI) + hooks.wifi_scan = tedge_provisioning_wifi_scan; +#endif +#if defined(HAVE_LED0) || defined(HAVE_LED_STRIP) + hooks.identify = identify; +#endif + hooks.chip = CONFIG_SOC; + + if (overrides != NULL) { + if (overrides->wifi_connect != NULL) { + hooks.wifi_connect = overrides->wifi_connect; + } + if (overrides->identify != NULL) { + hooks.identify = overrides->identify; + } + if (overrides->wifi_scan != NULL) { + hooks.wifi_scan = overrides->wifi_scan; + } + if (overrides->chip != NULL) { + hooks.chip = overrides->chip; + } + if (overrides->firmware_name != NULL) { + hooks.firmware_name = overrides->firmware_name; + } + if (overrides->firmware_version != NULL) { + hooks.firmware_version = overrides->firmware_version; + } + hooks.ctx = overrides->ctx; + } + if (hooks.wifi_connect == NULL) { + LOG_WRN("provisioning: no Wi-Fi hook (CONFIG_WIFI_CREDENTIALS is " + "off): Wi-Fi settings will be refused"); + } + return tedge_provisioning_init(&hooks); +} From 8388bb6988952f556efc03f2b514785e679c0202 Mon Sep 17 00:00:00 2001 From: reubenmiller Date: Tue, 10 Nov 2026 17:16:17 +1000 Subject: [PATCH 2/2] docs(zephyr): setting a device up over USB The Zephyr port's README describes the provisioning library, the port's default hooks, and how an application replaces them. The spec gains the default hooks as a requirement. Co-Authored-By: Claude Opus 5.5 --- .../specs/tedge-provisioning/spec.md | 22 ++++++++++++ ports/zephyr/README.md | 36 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/openspec/changes/serial-onboarding/specs/tedge-provisioning/spec.md b/openspec/changes/serial-onboarding/specs/tedge-provisioning/spec.md index fe0f9aa..dbc6a3f 100644 --- a/openspec/changes/serial-onboarding/specs/tedge-provisioning/spec.md +++ b/openspec/changes/serial-onboarding/specs/tedge-provisioning/spec.md @@ -82,6 +82,28 @@ Wi-Fi credentials itself. It SHALL judge success by the network coming up - **THEN** the application joins the network and stores the credentials in its own way, and the library reports "provisioned" when the network is up +### Requirement: The Zephyr port provides default hooks + +The Zephyr port SHALL provide `tedge_provisioning_zephyr_init()`, which +starts the library with the port's hooks: Wi-Fi through Zephyr's Wi-Fi +credentials store, the scan, identify on the devicetree's `led0` or +`led-strip`, and `CONFIG_SOC` as the chip. Each hook the application passes +SHALL replace the port's. The port's Wi-Fi hook SHALL store a network only +once the device has an address on it, and SHALL rejoin the previously +stored network when the new one cannot be joined. + +#### Scenario: Application with its own network management + +- **WHEN** an application passes its own Wi-Fi hook to + `tedge_provisioning_zephyr_init()` +- **THEN** the library uses that hook, and the port's scan, identify and + chip + +#### Scenario: Wrong password with a network already stored + +- **WHEN** the port's Wi-Fi hook cannot join the new network +- **THEN** the stored network is unchanged, and the device joins it again + ### Requirement: The tiny-edge info command reports what onboarding needs The library SHALL answer a tiny-edge info command (0xE0) with the diff --git a/ports/zephyr/README.md b/ports/zephyr/README.md index 2d473de..1019181 100644 --- a/ports/zephyr/README.md +++ b/ports/zephyr/README.md @@ -635,6 +635,42 @@ DNS from DHCP or runs the mDNS responder It needs nothing from the application beyond UDP; each lookup holds one socket while it runs. +### Setting a device up over USB (tedge-onboard) + +With `CONFIG_TEDGE_PROVISIONING=y`, a device can be set up from a computer +it is plugged into: [`tedge-onboard`](../../tools/onboard/README.md) gives it +a Wi-Fi network, registers it in Cumulocity, and passes it the tenant and a +one-time password. The library (`include/tedge/provisioning.h`) answers the +Improv Wi-Fi protocol on the console; with the shell on the console +(`CONFIG_SHELL_BACKEND_SERIAL`) the tool borrows it with `tedge improv`, +otherwise the console speaks Improv from boot. + +Start it after `tedge_init()`, with the port's ready-made hooks: + +```c +#include + +tedge_init(NULL, &hooks); +tedge_provisioning_zephyr_init(NULL); +tedge_start(); +``` + +| Hook | The port's default | +|---|---| +| Wi-Fi | Joins the network, keeps it in Zephyr's Wi-Fi credentials store (`CONFIG_WIFI_CREDENTIALS`) only once the device has an address, and goes back to the previous network otherwise | +| Scan | The networks the first Wi-Fi interface sees | +| Identify | Blinks the devicetree's `led0`, or the first pixel of its `led-strip` | +| Chip | `CONFIG_SOC` | + +An application that manages its network itself (its own reconnect logic), +or has a better light, passes the hooks it wants to replace: +`tedge_provisioning_zephyr_init(&mine)`. Joining a stored network at boot +stays the application's: the minimal sample does it with +`NET_REQUEST_WIFI_CONNECT_STORED` (`samples/minimal/overlay-onboarding.conf`). + +The library adds about 5 KB of flash and 5 KB of RAM (4 KB of it the +worker's stack). + ### Security limitations - The device key is stored in PSA ITS, encrypted with a key derived from the