From c5a2bafb1cf2aab10c17a4e23c80834624c13884 Mon Sep 17 00:00:00 2001 From: Gilberto Conti Date: Mon, 18 May 2026 16:32:12 +0200 Subject: [PATCH 1/4] loader: add ota support On boot, install /ota:/UPDATE_SKETCH.BIN into the user_sketch partition when the file is present. Validates the inner sketch header and bounds, and keeps UPDATE_SKETCH.BIN on post-erase failure so the next boot can retry. Signed-off-by: Gilberto Conti --- boards.txt | 15 +++ cores/arduino/llext_wrappers.c | 2 + cores/arduino/ota_api.h | 11 ++ extra/build.sh | 3 + loader/CMakeLists.txt | 2 + loader/Kconfig | 2 + loader/llext_exports.c | 4 + loader/main.c | 5 + loader/ota/CMakeLists.txt | 9 ++ loader/ota/Kconfig | 43 ++++++++ loader/ota/ota_api.h | 20 ++++ loader/ota/ota_llext_exports.c | 11 ++ loader/ota/ota_sketch.c | 42 ++++++++ loader/ota/ota_sketch_check.c | 185 +++++++++++++++++++++++++++++++++ loader/ota/ota_sketch_check.h | 28 +++++ platform.txt | 5 + 16 files changed, 387 insertions(+) create mode 100644 cores/arduino/ota_api.h create mode 100644 loader/ota/CMakeLists.txt create mode 100644 loader/ota/Kconfig create mode 100644 loader/ota/ota_api.h create mode 100644 loader/ota/ota_llext_exports.c create mode 100644 loader/ota/ota_sketch.c create mode 100644 loader/ota/ota_sketch_check.c create mode 100644 loader/ota/ota_sketch_check.h diff --git a/boards.txt b/boards.txt index 798564387..e65001f2a 100644 --- a/boards.txt +++ b/boards.txt @@ -72,6 +72,9 @@ giga.debug.server.openocd.scripts.2=target/stm32h7x_dual_bank.cfg giga.debug.cortex-debug.custom.request=attach giga.debug.svd_file={runtime.platform.path}/svd/STM32H747_CM7.svd +giga.build.ota.magic=0x23410266 +giga.build.ota.sketch_offset={bootloader.maximum_size} + ########################################################################################## nano33ble.name=Arduino Nano 33 BLE @@ -256,6 +259,9 @@ portentah7.bootloader.interface=0 portentah7.bootloader.file=zephyr-{build.variant}.bin portentah7.bootloader.address=0x08040000 +portentah7.build.ota.magic=0x2341025B +portentah7.build.ota.sketch_offset={bootloader.maximum_size} + portentah7.debug.tool=gdb portentah7.debug.server.openocd.scripts.0=interface/{programmer.protocol}.cfg portentah7.debug.server.openocd.scripts.1={programmer.transport_script} @@ -308,6 +314,9 @@ nicla_vision.bootloader.interface=0 nicla_vision.bootloader.file=zephyr-{build.variant}.bin nicla_vision.bootloader.address=0x08040000 +nicla_vision.build.ota.magic=0x2341025F +nicla_vision.build.ota.sketch_offset={bootloader.maximum_size} + nicla_vision.debug.tool=gdb nicla_vision.debug.server.openocd.scripts.0=interface/{programmer.protocol}.cfg nicla_vision.debug.server.openocd.scripts.1={programmer.transport_script} @@ -458,6 +467,9 @@ portentac33.leave.dfuse={empty} -Q portentac33.loader.args.with_sketch={empty} -w portentac33.sketch.args.without_loader={empty} -w +portentac33.build.ota.magic=0x23410068 +portentac33.build.ota.sketch_offset={bootloader.maximum_size} + ########################################################################################## opta.name=Arduino Opta @@ -510,6 +522,9 @@ opta.debug.server.openocd.scripts.2=target/stm32h7x_dual_bank.cfg opta.debug.cortex-debug.custom.request=attach opta.debug.svd_file={runtime.platform.path}/svd/STM32H747_CM7.svd +opta.build.ota.magic=0x23410064 +opta.build.ota.sketch_offset={bootloader.maximum_size} + ########################################################################################## nano_matter.name=Arduino Nano Matter diff --git a/cores/arduino/llext_wrappers.c b/cores/arduino/llext_wrappers.c index 0ea624939..8168076c8 100644 --- a/cores/arduino/llext_wrappers.c +++ b/cores/arduino/llext_wrappers.c @@ -109,6 +109,7 @@ W2(char *, strstr, const char *, const char *) W2(int, strcmp, const char *, const char *) W3(int, strncmp, const char *, const char *, size_t) W2(int, strcasecmp, const char *, const char *) +W3(int, strncasecmp, const char *, const char *, size_t) W3(char *, strncpy, char *, const char *, size_t) W2(char *, strcat, char *, const char *) W2(char *, strcpy, char *, const char *) @@ -216,6 +217,7 @@ VN(__aeabi_fcmpun) VN(__aeabi_d2iz) VN(__aeabi_d2uiz) VN(__aeabi_d2lz) +VN(__aeabi_d2ulz) VN(__aeabi_i2d) VN(__aeabi_ui2d) VN(__aeabi_l2d) diff --git a/cores/arduino/ota_api.h b/cores/arduino/ota_api.h new file mode 100644 index 000000000..e97bf3460 --- /dev/null +++ b/cores/arduino/ota_api.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifdef CONFIG_OTA + +#include "../../loader/ota/ota_api.h" + +#endif // CONFIG_OTA diff --git a/extra/build.sh b/extra/build.sh index 4678b134a..93996022b 100755 --- a/extra/build.sh +++ b/extra/build.sh @@ -214,6 +214,9 @@ if [ ! -z "$board" ]; then fi update_local_field "upload.address" "$UPLOAD_ADDR" "$UPLOAD_ADDR_COMMENT" + LOADER_MAX_SIZE=$(get_value_from_text_file variants/${variant}/syms-static.ld '_loader_max_size') + update_local_field "bootloader.maximum_size" $LOADER_MAX_SIZE + # maximum sketch size: size of sketch partition, decimal (exact limit) CODE_SIZE=$(( $(get_value_from_text_file variants/${variant}/syms-static.ld '_sketch_max_size') )) update_local_field "upload.maximum_size" $CODE_SIZE diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt index 5e6f2c9f2..d132ba12d 100644 --- a/loader/CMakeLists.txt +++ b/loader/CMakeLists.txt @@ -18,6 +18,7 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(app LANGUAGES C CXX) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/blobs) +add_subdirectory_ifdef(CONFIG_OTA ${CMAKE_CURRENT_SOURCE_DIR}/ota) # Memory region relocation based on Kconfig # Relocate LLEXT heap if region specified @@ -44,3 +45,4 @@ target_compile_definitions(app PUBLIC _XOPEN_SOURCE=700) target_compile_definitions(app PUBLIC _GNU_SOURCE) target_link_libraries(app PUBLIC blobs) +target_link_libraries_ifdef(CONFIG_OTA app PUBLIC ota) diff --git a/loader/Kconfig b/loader/Kconfig index 9965f3757..31b291958 100644 --- a/loader/Kconfig +++ b/loader/Kconfig @@ -23,3 +23,5 @@ config MAIN_STACK_REGION depends on CODE_DATA_RELOCATION help Specify the memory region for main stack. + +rsource "ota/Kconfig" diff --git a/loader/llext_exports.c b/loader/llext_exports.c index 4c2c6e8a9..587b41950 100644 --- a/loader/llext_exports.c +++ b/loader/llext_exports.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,7 @@ EXPORT_LIBC_SYM(strstr); EXPORT_LIBC_SYM(strncmp); EXPORT_LIBC_SYM(strncpy); EXPORT_LIBC_SYM(strcasecmp); +EXPORT_LIBC_SYM(strncasecmp); EXPORT_LIBC_SYM(strcmp); EXPORT_LIBC_SYM(strlen); EXPORT_LIBC_SYM(strnlen); @@ -349,6 +351,7 @@ EXPORT_LIBC_SYM(gettimeofday); #endif EXPORT_SYMBOL(sys_clock_settime); + EXPORT_SYMBOL(mktime); EXPORT_SYMBOL(gmtime); @@ -409,6 +412,7 @@ EXPORT_AEABI_SYM(__aeabi_fcmpun); EXPORT_AEABI_SYM(__aeabi_d2iz); EXPORT_AEABI_SYM(__aeabi_d2uiz); EXPORT_AEABI_SYM(__aeabi_d2lz); +EXPORT_AEABI_SYM(__aeabi_d2ulz); EXPORT_AEABI_SYM(__aeabi_i2d); EXPORT_AEABI_SYM(__aeabi_ui2d); EXPORT_AEABI_SYM(__aeabi_l2d); diff --git a/loader/main.c b/loader/main.c index 0947559ea..c8fa42fa2 100644 --- a/loader/main.c +++ b/loader/main.c @@ -24,6 +24,7 @@ LOG_MODULE_REGISTER(sketch); #include #include "../cores/arduino/zephyr_sketch_header.h" +#include "ota/ota_sketch_check.h" #define SKETCH_RAM_BUFFER_LEN 131072 @@ -136,6 +137,10 @@ static int loader(const struct shell *sh) { return rc; } +#if defined(CONFIG_OTA) + try_ota_update(fa); +#endif + uintptr_t base_addr = DT_PARTITION_ADDR(DT_NODELABEL(user_sketch)); char header[SKETCH_HEADER_LEN]; diff --git a/loader/ota/CMakeLists.txt b/loader/ota/CMakeLists.txt new file mode 100644 index 000000000..0229b0137 --- /dev/null +++ b/loader/ota/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_interface_library_named(ota) + +target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_sketch_check.c) +target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_llext_exports.c) +zephyr_include_directories(${CMAKE_CURRENT_LIST_DIR}) + +target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_sketch.c) diff --git a/loader/ota/Kconfig b/loader/ota/Kconfig new file mode 100644 index 000000000..7c3b6a2b1 --- /dev/null +++ b/loader/ota/Kconfig @@ -0,0 +1,43 @@ +# +# Copyright (c) Arduino s.r.l. and/or its affiliated companies +# +# SPDX-License-Identifier: Apache-2.0 +# + +menuconfig OTA + bool "Enable lookup for sketch update at boot" + depends on FILE_SYSTEM + select REBOOT + select RTC if SOC_SERIES_STM32H7X + help + Enable checking for an update inside of the filesystem. If a file inside of + OTA_SKETCH_UPDATE_PATH is found during the boot, the OTA procedure is started and the + current sketch binary is replaced + +if OTA + +config OTA_SKETCH_UPDATE_PATH + string "The path it is looked for ota" + default "/ota:/UPDATE_SKETCH.BIN" + help + the path that is checked for a sketch new binary + +config OTA_SKETCH_TEMP_PATH_POSTFIX + string "temporary file name postfix for sketch ota not ready" + default ".TEMP" + help + String that is appended to sketch ota filename when it is still not ready to be loaded, + a rename operation is required to make the ota file enabled for next reboot + +config OTA_TRANSCRIPTION_CHUNK_SIZE + int "The size of the stack allocated chunk in bytes for transcription" + default 1024 + help + The size of the stack allocated chunk in bytes for ota file transcription + +module = OTA +module-str = OTA +module-help = Sets log level for OTA +source "subsys/logging/Kconfig.template.log_config" + +endif diff --git a/loader/ota/ota_api.h b/loader/ota/ota_api.h new file mode 100644 index 000000000..d30faba9a --- /dev/null +++ b/loader/ota/ota_api.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define OTA_SKETCH_FILENAME CONFIG_OTA_SKETCH_UPDATE_PATH CONFIG_OTA_SKETCH_TEMP_PATH_POSTFIX + +int ota_sketch_ready(); +int ota_sketch_start(); + +#ifdef __cplusplus +} +#endif diff --git a/loader/ota/ota_llext_exports.c b/loader/ota/ota_llext_exports.c new file mode 100644 index 000000000..c0e069f55 --- /dev/null +++ b/loader/ota/ota_llext_exports.c @@ -0,0 +1,11 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ota_api.h" +#include + +EXPORT_SYMBOL(ota_sketch_ready); +EXPORT_SYMBOL(ota_sketch_start); diff --git a/loader/ota/ota_sketch.c b/loader/ota/ota_sketch.c new file mode 100644 index 000000000..6014b737e --- /dev/null +++ b/loader/ota/ota_sketch.c @@ -0,0 +1,42 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "ota_api.h" + +#include +LOG_MODULE_REGISTER(ota_sketch, CONFIG_OTA_LOG_LEVEL); + +int ota_sketch_ready() { + struct fs_dirent entry; + int ret; + + if (fs_stat(OTA_SKETCH_FILENAME, &entry) < 0) { + LOG_WRN("tried to perform an ota without binary present"); + return -1; + } + + if (sizeof(CONFIG_OTA_SKETCH_TEMP_PATH_POSTFIX) > 1) { + ret = fs_rename(OTA_SKETCH_FILENAME, CONFIG_OTA_SKETCH_UPDATE_PATH); + + if (ret < 0) { + LOG_ERR("unable to rename ota file: %d", ret); + } + } + + LOG_INF("OTA marked as ready"); + + return ret; +} + +int ota_sketch_start() { + LOG_INF("OTA started, reboot in progress"); + + sys_reboot(SYS_REBOOT_COLD); + return 0; +} diff --git a/loader/ota/ota_sketch_check.c b/loader/ota/ota_sketch_check.c new file mode 100644 index 000000000..be5b785fb --- /dev/null +++ b/loader/ota/ota_sketch_check.c @@ -0,0 +1,185 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(ota, CONFIG_OTA_LOG_LEVEL); + +#include "ota_sketch_check.h" +#include "../../cores/arduino/zephyr_sketch_header.h" + +#include +#include +#include + +#include + +static int verify_sketch_header(const struct flash_area *fa, const struct sketch_header_v1 *hdr, + struct fs_dirent *entry) { + int rc = 0; + + LOG_DBG("Header ver: %02x, len: %d, magic: %04x, flags: %02X", hdr->ver, hdr->len, hdr->magic, + hdr->flags); + + if (sketch_header_v1_verify(hdr) != 0) { + LOG_ERR("invalid sketch header (ver=0x%x magic=0x%x)", hdr->ver, hdr->magic); + return -1; + } + + LOG_INF("sketch length = %u bytes", (unsigned)hdr->len); + + /* Bounds-check before the destructive erase. */ + if (hdr->len > fa->fa_size) { + LOG_ERR("sketch too large for partition (%u > %u)", (unsigned)hdr->len, + (unsigned)fa->fa_size); + return -1; + } + + if ((off_t)hdr->len > entry->size) { + LOG_ERR("header len exceeds file size (%u > %ld)", (unsigned)hdr->len, (long)entry->size); + return -1; + } + + return rc; +} + +static int perform_ota(const struct flash_area *fa, struct fs_file_t *file, + const struct sketch_header_v1 *hdr) { + int rc = 0; + + size_t wbs = flash_area_align(fa) > 0 ? flash_area_align(fa) : 1; + + uint8_t chunk[CONFIG_OTA_TRANSCRIPTION_CHUNK_SIZE]; + __ASSERT(wbs < CONFIG_OTA_TRANSCRIPTION_CHUNK_SIZE, + "OTA internal buffer is smaller(%d) than write block size(%d)", + CONFIG_OTA_TRANSCRIPTION_CHUNK_SIZE, wbs); + + __ASSERT(CONFIG_OTA_TRANSCRIPTION_CHUNK_SIZE % wbs == 0, + "OTA internal buffer(%d) is not multiple of write block " + "size(%d)" CONFIG_OTA_TRANSCRIPTION_CHUNK_SIZE, + wbs); + + off_t offset = 0; + size_t remaining = hdr->len; + ssize_t n; + size_t wlen; + + LOG_INF("erasing flash partition (%u bytes)...", (unsigned)fa->fa_size); + rc = flash_area_erase(fa, 0, fa->fa_size); + + if (rc) { + LOG_ERR("flash erase failed, rc %d — retry on next boot", rc); + goto exit; + } + + if ((rc = fs_seek(file, 0, FS_SEEK_SET))) { + LOG_ERR("failed to seek to start of file, rc %d — retry on next boot", rc); + goto exit; + } + + LOG_INF("rewriting sketch binary"); + + while (remaining > 0 && + (n = fs_read(file, chunk, remaining < sizeof(chunk) ? remaining : sizeof(chunk))) > 0) { + wlen = (size_t)n; + /* Pad the final partial chunk to the write-block-size with 0xFF + * (erased value); flash rejects unaligned writes. */ + if (wlen % wbs != 0) { + memset(chunk + wlen, 0xff, ROUND_UP(wlen, wbs) - wlen); + wlen = ROUND_UP(wlen, wbs); + } + + if ((rc = flash_area_write(fa, offset, chunk, wlen))) { + LOG_ERR("flash write failed at offset %ld, rc %d — retry on next boot", (long)offset, + rc); + + goto exit; + } + offset += n; + remaining -= n; + } + + if (remaining > 0) { + LOG_ERR("short read, %u bytes missing — retry on next boot", (unsigned)remaining); + goto exit; + } + + LOG_INF("wrote %ld bytes to flash", (long)offset); +exit: + return rc; +} + +int try_ota_update(const struct flash_area *fa) { + int rc = 0; + + struct fs_file_t file = {0}; + struct fs_dirent entry; + + struct sketch_header_v1 *hdr = NULL; + char header[SKETCH_HEADER_LEN]; + bool delete = false; + + LOG_DBG("checking for ota in \"%s\"", CONFIG_OTA_SKETCH_UPDATE_PATH); + + if (fa == NULL) { + rc = -1; + goto exit; + } + + if (fs_stat(CONFIG_OTA_SKETCH_UPDATE_PATH, &entry) != 0) { + LOG_DBG("no update pending"); + goto exit; + } + + LOG_INF("update pending, validating..."); + + fs_file_t_init(&file); + rc = fs_open(&file, CONFIG_OTA_SKETCH_UPDATE_PATH, FS_O_READ); + if (rc < 0) { + LOG_ERR("failed to open %s, rc %d", CONFIG_OTA_SKETCH_UPDATE_PATH, rc); + delete = true; + goto exit; + } + + if (entry.size < SKETCH_HEADER_LEN) { + LOG_ERR("file too small (%ld bytes)", (long)entry.size); + delete = true; + rc = -1; + goto exit; + } + + rc = fs_read(&file, header, SKETCH_HEADER_LEN); + if (rc != SKETCH_HEADER_LEN) { + LOG_ERR("failed to read header"); + delete = true; + rc = -1; + goto exit; + } + + hdr = (struct sketch_header_v1 *)(header + 7); + if ((rc = verify_sketch_header(fa, hdr, &entry))) { + delete = true; + goto exit; + } + + /* Partition becomes inconsistent past here; keep UPDATE.BIN on failure. */ + LOG_INF("performing ota"); + if ((rc = perform_ota(fa, &file, hdr))) { + goto exit; + } + delete = true; + LOG_INF("update complete"); +exit: + + if (file.mp != NULL && (rc = fs_close(&file))) { + LOG_ERR("error on fs_close %d", rc); + } + + if (delete && (rc = fs_unlink(CONFIG_OTA_SKETCH_UPDATE_PATH))) { + LOG_ERR("error on fs_unlink %d", rc); + } + + return rc; +} diff --git a/loader/ota/ota_sketch_check.h b/loader/ota/ota_sketch_check.h new file mode 100644 index 000000000..fa750c76f --- /dev/null +++ b/loader/ota/ota_sketch_check.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * This function is meant to be used by the loader to check for a sketch ota update + * + * Install /ota:/UPDATE.BIN into the sketch partition if present. The sketch + * validates the OTA payload (magic, CRC32, decompression) before writing it, + * so the loader trusts the file and only re-checks the inner sketch_header_v1. + * + * On failure before the erase, UPDATE.BIN is removed and the existing sketch + * boots. On failure after it, the partition is already partial so UPDATE.BIN + * is kept and the next boot retries — the only way back without DFU. + */ +int try_ota_update(const struct flash_area *fa); + +#ifdef __cplusplus +} +#endif diff --git a/platform.txt b/platform.txt index 4eee89658..bc8821cf4 100644 --- a/platform.txt +++ b/platform.txt @@ -69,6 +69,10 @@ build.link_mode=dynamic build.boot_mode=wait upload.extension=elf-zsk.bin +# OTA generation defaults. Boards opt in by setting build.ota.magic and +# build.ota.sketch_offset; with magic empty, postobjcopy.3 is a no-op. +build.ota.magic= +build.ota.sketch_offset= build.ldscript.path={runtime.platform.path}/variants/_ldscripts build.link_command="{compiler.path}{compiler.c.elf.cmd}" "-L{build.path}" "-L{build.variant.path}" {compiler.c.elf.flags} {compiler.c.elf.extra_flags} {build.extra_flags} {build.extra_ldflags} {compiler.zephyr.common_ldflags} {compiler.ldflags} {object_files} -Wl,--start-group "{build.path}/{archive_file}" {compiler.zephyr.extra_ldflags} {compiler.libraries.ldflags} -Wl,--end-group {build.link_args.{build.link_mode}} @@ -163,6 +167,7 @@ recipe.objcopy.hex.pattern="{compiler.path}{compiler.elf2hex.cmd}" {compiler.elf ## Mangle the file recipe.hooks.objcopy.postobjcopy.1.pattern="{runtime.tools.zephyr-sketch-tool.path}/zephyr-sketch-tool" {build.zsk_args.debug} {build.zsk_args.mode-{build.link_mode}} {build.zsk_args.startup-mode-{build.boot_mode}} "{build.path}/{build.project_name}.elf" recipe.hooks.objcopy.postobjcopy.2.pattern="{runtime.tools.zephyr-sketch-tool.path}/zephyr-sketch-tool" {build.zsk_args.debug} {build.zsk_args.mode-{build.link_mode}} {build.zsk_args.startup-mode-{build.boot_mode}} "{build.path}/{build.project_name}.bin" +recipe.hooks.objcopy.postobjcopy.3.pattern="{runtime.tools.zephyr-sketch-tool.path}/zephyr-sketch-tool" -ota -ota-loader "{runtime.platform.path}/firmwares/zephyr-{build.variant}.bin" -ota-sketch "{build.path}/{build.project_name}.{upload.extension}" -ota-offset {build.ota.sketch_offset} -ota-magic {build.ota.magic} -output "{build.path}/{build.project_name}" ## Compute size recipe.size.pattern="{runtime.tools.zephyr-check-size.path}/zephyr-check-size" {build.link_mode} "{runtime.platform.path}/firmwares/zephyr-{build.variant}.config" "{build.path}/{build.project_name}.{upload.extension}" "{build.path}/{build.project_name}.elf" From 4a433139d73215534a6f711d9a2989e70d57bd72 Mon Sep 17 00:00:00 2001 From: Andrea Gilardoni Date: Wed, 23 Sep 2026 09:37:53 +0200 Subject: [PATCH 2/4] variants: ota: enable ota support enable ota support for tested boards Signed-off-by: Andrea Gilardoni --- .../arduino_giga_r1_stm32h747xx_m7.conf | 2 ++ .../arduino_nicla_vision_stm32h747xx_m7.conf | 2 ++ .../arduino_opta_stm32h747xx_m7.conf | 2 ++ .../arduino_portenta_h7_stm32h747xx_m7.conf | 2 ++ 4 files changed, 8 insertions(+) diff --git a/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf b/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf index bb0db0fd7..5a5bdee72 100644 --- a/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf +++ b/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf @@ -212,3 +212,5 @@ CONFIG_NET_CONFIG_AUTO_INIT=n CONFIG_NET_CONNECTION_MANAGER=y #CONFIG_WIFI_NM_MAX_MANAGED_INTERFACES=2 CONFIG_NET_L2_ETHERNET_MGMT=y + +CONFIG_OTA=y diff --git a/variants/arduino_nicla_vision_stm32h747xx_m7/arduino_nicla_vision_stm32h747xx_m7.conf b/variants/arduino_nicla_vision_stm32h747xx_m7/arduino_nicla_vision_stm32h747xx_m7.conf index f6c0395c4..9e3f6191f 100644 --- a/variants/arduino_nicla_vision_stm32h747xx_m7/arduino_nicla_vision_stm32h747xx_m7.conf +++ b/variants/arduino_nicla_vision_stm32h747xx_m7/arduino_nicla_vision_stm32h747xx_m7.conf @@ -193,3 +193,5 @@ CONFIG_VIDEO_BUFFER_POOL_ALIGN=32 CONFIG_VIDEO_GC2145=y CONFIG_FLASH=y + +CONFIG_OTA=y diff --git a/variants/arduino_opta_stm32h747xx_m7/arduino_opta_stm32h747xx_m7.conf b/variants/arduino_opta_stm32h747xx_m7/arduino_opta_stm32h747xx_m7.conf index df52fe27c..292ef12b6 100644 --- a/variants/arduino_opta_stm32h747xx_m7/arduino_opta_stm32h747xx_m7.conf +++ b/variants/arduino_opta_stm32h747xx_m7/arduino_opta_stm32h747xx_m7.conf @@ -191,3 +191,5 @@ CONFIG_BT_RX_STACK_SIZE=4096 CONFIG_STM32H7_BOOT_M4_AT_INIT=n CONFIG_I2C_STM32_V2_TIMING=y + +CONFIG_OTA=y diff --git a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf index 7d5555dca..0dab568f0 100644 --- a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf +++ b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf @@ -204,3 +204,5 @@ CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 CONFIG_BT_RX_STACK_SIZE=4096 CONFIG_STM32H7_BOOT_M4_AT_INIT=n + +CONFIG_OTA=y From 979339d25cc98e537c05bd7b2946543eba11d2c7 Mon Sep 17 00:00:00 2001 From: Andrea Gilardoni Date: Tue, 22 Sep 2026 08:50:01 +0200 Subject: [PATCH 3/4] ota: implement ota for loader binary extend the ota api to allow performing loader updates by means of bootloader --- loader/llext_exports.c | 1 - loader/ota/CMakeLists.txt | 3 ++ loader/ota/Kconfig | 14 ++++++++ loader/ota/ota_api.h | 4 +++ loader/ota/ota_arduino_stm32h7.c | 55 ++++++++++++++++++++++++++++++++ loader/ota/ota_llext_exports.c | 3 ++ 6 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 loader/ota/ota_arduino_stm32h7.c diff --git a/loader/llext_exports.c b/loader/llext_exports.c index 587b41950..7f80c91d1 100644 --- a/loader/llext_exports.c +++ b/loader/llext_exports.c @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include diff --git a/loader/ota/CMakeLists.txt b/loader/ota/CMakeLists.txt index 0229b0137..1a7514086 100644 --- a/loader/ota/CMakeLists.txt +++ b/loader/ota/CMakeLists.txt @@ -6,4 +6,7 @@ target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_sketch_check.c) target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_llext_exports.c) zephyr_include_directories(${CMAKE_CURRENT_LIST_DIR}) +target_sources_ifdef(CONFIG_SOC_SERIES_STM32H7X app PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/ota_arduino_stm32h7.c) + target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_sketch.c) diff --git a/loader/ota/Kconfig b/loader/ota/Kconfig index 7c3b6a2b1..3357cee5b 100644 --- a/loader/ota/Kconfig +++ b/loader/ota/Kconfig @@ -29,6 +29,20 @@ config OTA_SKETCH_TEMP_PATH_POSTFIX String that is appended to sketch ota filename when it is still not ready to be loaded, a rename operation is required to make the ota file enabled for next reboot +config OTA_LOADER_UPDATE_PATH + string "The path it is looked for ota" + default "/ota:/UPDATE.BIN" + help + the path that is checked for the loader new binary + +config OTA_LOADER_TEMP_PATH_POSTFIX + string "temporary file name postfix for loader ota not ready" + default "" if SOC_SERIES_STM32H7X + default ".TEMP" + help + String that is appended to loader ota filename when it is still not ready to be loaded, + a rename operation is required to make the ota file enabled for next reboot + config OTA_TRANSCRIPTION_CHUNK_SIZE int "The size of the stack allocated chunk in bytes for transcription" default 1024 diff --git a/loader/ota/ota_api.h b/loader/ota/ota_api.h index d30faba9a..d84ecc029 100644 --- a/loader/ota/ota_api.h +++ b/loader/ota/ota_api.h @@ -11,10 +11,14 @@ extern "C" { #endif #define OTA_SKETCH_FILENAME CONFIG_OTA_SKETCH_UPDATE_PATH CONFIG_OTA_SKETCH_TEMP_PATH_POSTFIX +#define OTA_LOADER_FILENAME CONFIG_OTA_LOADER_UPDATE_PATH CONFIG_OTA_LOADER_TEMP_PATH_POSTFIX int ota_sketch_ready(); int ota_sketch_start(); +int ota_loader_ready(); +int ota_loader_start(); + #ifdef __cplusplus } #endif diff --git a/loader/ota/ota_arduino_stm32h7.c b/loader/ota/ota_arduino_stm32h7.c new file mode 100644 index 000000000..739ae02af --- /dev/null +++ b/loader/ota/ota_arduino_stm32h7.c @@ -0,0 +1,55 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ota_api.h" +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(ota_loader, CONFIG_OTA_LOG_LEVEL); + +#define OTA_MAGIC_BOOT 0x07AA +// #define OTA_STORAGE_TYPE 0xA4 // QSPI_FLASH | FATFS | MBR +#define OTA_STORAGE_TYPE ((1 << 2) | (1 << 5) | (1 << 7)) +#define OTA_MBR_PART 2 + +int ota_loader_ready() { + struct fs_dirent entry; + int ret; + + if (fs_stat(OTA_LOADER_FILENAME, &entry) < 0) { + LOG_WRN("tried to perform an ota without binary present"); + return -1; + } + + if (sizeof(CONFIG_OTA_LOADER_TEMP_PATH_POSTFIX) > 1) { + ret = fs_rename(OTA_LOADER_FILENAME, CONFIG_OTA_LOADER_UPDATE_PATH); + if (ret < 0) { + LOG_ERR("unable to rename ota file: %d", ret); + return ret; + } + } + + stm32_backup_domain_enable_access(); + LL_RTC_BAK_SetRegister(RTC, RTC_BKP_DR0, OTA_MAGIC_BOOT); + LL_RTC_BAK_SetRegister(RTC, RTC_BKP_DR1, OTA_STORAGE_TYPE); + LL_RTC_BAK_SetRegister(RTC, RTC_BKP_DR2, OTA_MBR_PART); + LL_RTC_BAK_SetRegister(RTC, RTC_BKP_DR3, entry.size); + stm32_backup_domain_disable_access(); + + LOG_INF("OTA marked as ready"); + + return 0; +} + +int ota_loader_start() { + LOG_INF("OTA started, reboot in progress"); + + sys_reboot(SYS_REBOOT_COLD); + return 0; // unreachable +} diff --git a/loader/ota/ota_llext_exports.c b/loader/ota/ota_llext_exports.c index c0e069f55..39947fc4e 100644 --- a/loader/ota/ota_llext_exports.c +++ b/loader/ota/ota_llext_exports.c @@ -9,3 +9,6 @@ EXPORT_SYMBOL(ota_sketch_ready); EXPORT_SYMBOL(ota_sketch_start); + +EXPORT_SYMBOL(ota_loader_ready); +EXPORT_SYMBOL(ota_loader_start); From 8c8303601ba6eeae13af31fb60ef13b8ad335b04 Mon Sep 17 00:00:00 2001 From: Gilberto Conti Date: Tue, 22 Sep 2026 14:30:27 +0200 Subject: [PATCH 4/4] libraries: OTA: wifi/eth sketch/loader update Signed-off-by: Gilberto Conti --- extra/artifacts/zephyr_main.only | 1 + libraries/OTA/README.md | 45 ++++ .../OTA/examples/OTAEthernet/OTAEthernet.ino | 191 +++++++++++++++++ .../OTAFullEthernet/OTAFullEthernet.ino | 195 +++++++++++++++++ .../OTA/examples/OTAFullWiFi/OTAFullWiFi.ino | 196 ++++++++++++++++++ .../examples/OTAFullWiFi/arduino_secrets.h | 7 +- libraries/OTA/examples/OTAWiFi/OTAWiFi.ino | 192 +++++++++++++++++ .../OTA/examples/OTAWiFi/arduino_secrets.h | 8 + libraries/OTA/library.properties | 9 + libraries/OTA/src/ota.h | 41 ++++ loader/ota/CMakeLists.txt | 1 + loader/ota/ota_api.h | 24 --- loader/ota/ota_arduino_stm32h7.c | 4 +- loader/ota/ota_llext_exports.c | 2 +- loader/ota/ota_sketch.c | 4 +- .../test_setup.sh | 2 + .../test_setup.sh | 1 + .../arduino_nano_connect_rp2040/test_setup.sh | 1 + .../test_setup.sh | 1 + .../test_setup.sh | 1 + .../test_setup.sh | 2 + .../test_setup.sh | 1 + 22 files changed, 897 insertions(+), 32 deletions(-) create mode 100644 libraries/OTA/README.md create mode 100644 libraries/OTA/examples/OTAEthernet/OTAEthernet.ino create mode 100644 libraries/OTA/examples/OTAFullEthernet/OTAFullEthernet.ino create mode 100644 libraries/OTA/examples/OTAFullWiFi/OTAFullWiFi.ino rename cores/arduino/ota_api.h => libraries/OTA/examples/OTAFullWiFi/arduino_secrets.h (59%) create mode 100644 libraries/OTA/examples/OTAWiFi/OTAWiFi.ino create mode 100644 libraries/OTA/examples/OTAWiFi/arduino_secrets.h create mode 100644 libraries/OTA/library.properties create mode 100644 libraries/OTA/src/ota.h delete mode 100644 loader/ota/ota_api.h diff --git a/extra/artifacts/zephyr_main.only b/extra/artifacts/zephyr_main.only index fcc034373..e09cf92e1 100644 --- a/extra/artifacts/zephyr_main.only +++ b/extra/artifacts/zephyr_main.only @@ -4,4 +4,5 @@ # Files listed here are included by this package only, and automatically # excluded by all other artifacts. +libraries/OTA/ libraries/PDM/ diff --git a/libraries/OTA/README.md b/libraries/OTA/README.md new file mode 100644 index 000000000..9b03cfa5a --- /dev/null +++ b/libraries/OTA/README.md @@ -0,0 +1,45 @@ +# OTA + +Downloads and installs firmware updates on an Arduino board running the +Zephyr core. + +**Sketch only** — the examples fetch a sketch binary over plain HTTP and +write it to the loader's staging area. On the next boot the Zephyr loader +installs it in place of the running sketch. + +**Loader + sketch** — the examples fetch a full binary (loader + sketch) +and write it to the OTA partition on QSPI. On the next boot the mbed +bootloader writes the entire payload to internal flash, updating both the +loader and the sketch in one shot. + +## Requirements + +- The loader must be built with `CONFIG_OTA=y`. +- Run `FlashFormat` (in `libraries/Storage/examples/`) once before using OTA. + It creates the MBR partition table and formats the FAT filesystems on QSPI + that both the Zephyr loader and the mbed bootloader depend on. + +## Producing the update images + +**Sketch only** — take the `.elf-zsk.bin` from the sketch build. + +**Loader + sketch** — take the `.elf-zsk.bin-bundle.bin` from the sketch +build. The build system produces it automatically; it is the raw loader +binary + 0xFF padding + sketch, without the OTA header. The mbed +bootloader validates the file by checking that the first word is a valid +ARM initial stack pointer; an OTA header would fail that check. + +Serve the file as `UPDATE.bin` with any HTTP server: + +``` +cp .elf-zsk.bin UPDATE.bin # sketch only +cp .elf-zsk.bin-bundle.bin UPDATE.bin # loader + sketch +python3 -m http.server 8000 +``` + +## Examples + +- **OTAEthernet** — sketch update over Ethernet (DHCP). +- **OTAWiFi** — sketch update over WiFi (edit `arduino_secrets.h`). +- **OTAFullEthernet** — loader + sketch update over Ethernet. +- **OTAFullWiFi** — loader + sketch update over WiFi. diff --git a/libraries/OTA/examples/OTAEthernet/OTAEthernet.ino b/libraries/OTA/examples/OTAEthernet/OTAEthernet.ino new file mode 100644 index 000000000..c96a1fd3f --- /dev/null +++ b/libraries/OTA/examples/OTAEthernet/OTAEthernet.ino @@ -0,0 +1,191 @@ +/* + * OTA sketch update over Ethernet. + * + * Downloads a raw .bin sketch image over plain HTTP and installs it on + * the next boot. No .ota container — the server hosts the plain + * ".elf-zsk.bin" artifact produced by the build. Serve it with: + * + * python3 -m http.server 8000 + * + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#ifndef CONFIG_OTA +#error "This variant's loader was built without CONFIG_OTA." +#endif + +#define STAGING_PATH CONFIG_OTA_SKETCH_UPDATE_PATH CONFIG_OTA_SKETCH_TEMP_PATH_POSTFIX + +const char *server = "10.10.10.10"; +const uint16_t port = 8000; +const char *path = "/UPDATE.bin"; + +ZephyrClient client; + +static long httpGet(Client &c, const char *host, uint16_t port, + const char *path, struct fs_file_t *out) { + if (!c.connect(host, port)) { + Serial.println("Connection failed"); + return -1; + } + + c.print("GET "); + c.print(path); + c.print(" HTTP/1.1\r\nHost: "); + c.print(host); + c.print("\r\nConnection: close\r\n\r\n"); + + char line[128]; + bool first = true; + long content_length = -1; + uint32_t last = millis(); + + while (true) { + if (!c.available()) { + if (!c.connected() || millis() - last > 10000) { + c.stop(); + return -1; + } + continue; + } + last = millis(); + + size_t n = c.readBytesUntil('\n', line, sizeof(line) - 1); + line[n] = '\0'; + if (n > 0 && line[n - 1] == '\r') { + line[n - 1] = '\0'; + } + + if (first) { + if (strncmp(line, "HTTP/1.", 7) != 0 || atoi(line + 9) != 200) { + Serial.print("HTTP error: "); + Serial.println(line); + c.stop(); + return -1; + } + first = false; + continue; + } + + if (line[0] == '\0') { + break; + } + + if (strncasecmp(line, "Content-Length:", 15) == 0) { + content_length = atol(line + 15); + } + } + + if (content_length <= 0) { + Serial.println("Missing or zero Content-Length"); + c.stop(); + return -1; + } + + uint8_t buf[512]; + long received = 0; + last = millis(); + + while (received < content_length) { + int avail = c.available(); + if (avail <= 0) { + if (!c.connected() || millis() - last > 10000) { + break; + } + continue; + } + + size_t want = (size_t)(content_length - received); + if (want > sizeof(buf)) { + want = sizeof(buf); + } + int got = c.read(buf, want); + if (got <= 0) { + continue; + } + + if (fs_write(out, buf, got) != got) { + Serial.println("Filesystem write failed"); + c.stop(); + return -1; + } + received += got; + last = millis(); + } + + c.stop(); + return received == content_length ? received : -1; +} + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("OTA update over Ethernet"); + + while (Ethernet.linkStatus() != LinkON) { + Serial.println("Waiting for link..."); + delay(100); + } + + if (Ethernet.begin() == 0) { + Serial.println("DHCP failed"); + return; + } + + Serial.print("IP: "); + Serial.println(Ethernet.localIP()); + + fs_unlink(STAGING_PATH); + + struct fs_file_t file; + fs_file_t_init(&file); + if (fs_open(&file, STAGING_PATH, FS_O_CREATE | FS_O_WRITE) < 0) { + Serial.println("Cannot open staging file"); + return; + } + + Serial.print("Downloading http://"); + Serial.print(server); + Serial.print(":"); + Serial.print(port); + Serial.println(path); + + long len = httpGet(client, server, port, path, &file); + fs_close(&file); + + if (len < 0) { + Serial.println("Download failed"); + fs_unlink(STAGING_PATH); + return; + } + + Serial.print("Downloaded "); + Serial.print(len); + Serial.println(" bytes"); + + if (ota_sketch_ready() < 0) { + Serial.println("Failed to mark update as ready"); + return; + } + + Serial.println("Rebooting..."); + Serial.flush(); + delay(100); + + ota_sketch_start(); +} + +void loop() { + delay(1000); +} diff --git a/libraries/OTA/examples/OTAFullEthernet/OTAFullEthernet.ino b/libraries/OTA/examples/OTAFullEthernet/OTAFullEthernet.ino new file mode 100644 index 000000000..9fd4aa2f6 --- /dev/null +++ b/libraries/OTA/examples/OTAFullEthernet/OTAFullEthernet.ino @@ -0,0 +1,195 @@ +/* + * Full OTA update: loader + sketch over Ethernet. + * + * Downloads a "-bundle.bin" file that the build system already produces + * (loader + 0xFF padding + sketch, without the OTA header). The mbed + * bootloader writes the payload to internal flash, updating both the + * loader and the sketch in one shot. + * + * Serve the file with any HTTP server, e.g.: + * + * cp Blink.ino.elf-zsk.bin-bundle.bin UPDATE.bin + * python3 -m http.server 8000 + * + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#ifndef CONFIG_OTA +#error "This variant's loader was built without CONFIG_OTA." +#endif + +#define STAGING_PATH CONFIG_OTA_LOADER_UPDATE_PATH CONFIG_OTA_LOADER_TEMP_PATH_POSTFIX + +const char *server = "10.10.10.10"; +const uint16_t port = 8000; +const char *path = "/UPDATE.bin"; + +ZephyrClient client; + +static long httpGet(Client &c, const char *host, uint16_t port, + const char *path, struct fs_file_t *out) { + if (!c.connect(host, port)) { + Serial.println("Connection failed"); + return -1; + } + + c.print("GET "); + c.print(path); + c.print(" HTTP/1.1\r\nHost: "); + c.print(host); + c.print("\r\nConnection: close\r\n\r\n"); + + char line[128]; + bool first = true; + long content_length = -1; + uint32_t last = millis(); + + while (true) { + if (!c.available()) { + if (!c.connected() || millis() - last > 10000) { + c.stop(); + return -1; + } + continue; + } + last = millis(); + + size_t n = c.readBytesUntil('\n', line, sizeof(line) - 1); + line[n] = '\0'; + if (n > 0 && line[n - 1] == '\r') { + line[n - 1] = '\0'; + } + + if (first) { + if (strncmp(line, "HTTP/1.", 7) != 0 || atoi(line + 9) != 200) { + Serial.print("HTTP error: "); + Serial.println(line); + c.stop(); + return -1; + } + first = false; + continue; + } + + if (line[0] == '\0') { + break; + } + + if (strncasecmp(line, "Content-Length:", 15) == 0) { + content_length = atol(line + 15); + } + } + + if (content_length <= 0) { + Serial.println("Missing or zero Content-Length"); + c.stop(); + return -1; + } + + uint8_t buf[512]; + long received = 0; + last = millis(); + + while (received < content_length) { + int avail = c.available(); + if (avail <= 0) { + if (!c.connected() || millis() - last > 10000) { + break; + } + continue; + } + + size_t want = (size_t)(content_length - received); + if (want > sizeof(buf)) { + want = sizeof(buf); + } + int got = c.read(buf, want); + if (got <= 0) { + continue; + } + + if (fs_write(out, buf, got) != got) { + Serial.println("Filesystem write failed"); + c.stop(); + return -1; + } + received += got; + last = millis(); + } + + c.stop(); + return received == content_length ? received : -1; +} + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("Full OTA update (loader + sketch) over Ethernet"); + + while (Ethernet.linkStatus() != LinkON) { + Serial.println("Waiting for link..."); + delay(100); + } + + if (Ethernet.begin() == 0) { + Serial.println("DHCP failed"); + return; + } + + Serial.print("IP: "); + Serial.println(Ethernet.localIP()); + + fs_unlink(STAGING_PATH); + + struct fs_file_t file; + fs_file_t_init(&file); + if (fs_open(&file, STAGING_PATH, FS_O_CREATE | FS_O_WRITE) < 0) { + Serial.println("Cannot open staging file"); + return; + } + + Serial.print("Downloading http://"); + Serial.print(server); + Serial.print(":"); + Serial.print(port); + Serial.println(path); + + long len = httpGet(client, server, port, path, &file); + fs_close(&file); + + if (len < 0) { + Serial.println("Download failed"); + fs_unlink(STAGING_PATH); + return; + } + + Serial.print("Downloaded "); + Serial.print(len); + Serial.println(" bytes"); + + if (ota_loader_ready() < 0) { + Serial.println("Failed to mark update as ready"); + return; + } + + Serial.println("Rebooting..."); + Serial.flush(); + delay(100); + + ota_loader_start(); +} + +void loop() { + delay(1000); +} diff --git a/libraries/OTA/examples/OTAFullWiFi/OTAFullWiFi.ino b/libraries/OTA/examples/OTAFullWiFi/OTAFullWiFi.ino new file mode 100644 index 000000000..a43718188 --- /dev/null +++ b/libraries/OTA/examples/OTAFullWiFi/OTAFullWiFi.ino @@ -0,0 +1,196 @@ +/* + * Full OTA update: loader + sketch over WiFi. + * + * Downloads a "-bundle.bin" file that the build system already produces + * (loader + 0xFF padding + sketch, without the OTA header). The mbed + * bootloader writes the payload to internal flash, updating both the + * loader and the sketch in one shot. + * + * Serve the file with any HTTP server, e.g.: + * + * cp Blink.ino.elf-zsk.bin-bundle.bin UPDATE.bin + * python3 -m http.server 8000 + * + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#ifndef CONFIG_OTA +#error "This variant's loader was built without CONFIG_OTA." +#endif + +#define STAGING_PATH CONFIG_OTA_LOADER_UPDATE_PATH CONFIG_OTA_LOADER_TEMP_PATH_POSTFIX + +#include "arduino_secrets.h" +char ssid[] = SECRET_SSID; +char pass[] = SECRET_PASS; + +const char *server = "10.10.10.10"; +const uint16_t port = 8000; +const char *path = "/UPDATE.bin"; + +ZephyrClient client; + +static long httpGet(Client &c, const char *host, uint16_t port, + const char *path, struct fs_file_t *out) { + if (!c.connect(host, port)) { + Serial.println("Connection failed"); + return -1; + } + + c.print("GET "); + c.print(path); + c.print(" HTTP/1.1\r\nHost: "); + c.print(host); + c.print("\r\nConnection: close\r\n\r\n"); + + char line[128]; + bool first = true; + long content_length = -1; + uint32_t last = millis(); + + while (true) { + if (!c.available()) { + if (!c.connected() || millis() - last > 10000) { + c.stop(); + return -1; + } + continue; + } + last = millis(); + + size_t n = c.readBytesUntil('\n', line, sizeof(line) - 1); + line[n] = '\0'; + if (n > 0 && line[n - 1] == '\r') { + line[n - 1] = '\0'; + } + + if (first) { + if (strncmp(line, "HTTP/1.", 7) != 0 || atoi(line + 9) != 200) { + Serial.print("HTTP error: "); + Serial.println(line); + c.stop(); + return -1; + } + first = false; + continue; + } + + if (line[0] == '\0') { + break; + } + + if (strncasecmp(line, "Content-Length:", 15) == 0) { + content_length = atol(line + 15); + } + } + + if (content_length <= 0) { + Serial.println("Missing or zero Content-Length"); + c.stop(); + return -1; + } + + uint8_t buf[512]; + long received = 0; + last = millis(); + + while (received < content_length) { + int avail = c.available(); + if (avail <= 0) { + if (!c.connected() || millis() - last > 10000) { + break; + } + continue; + } + + size_t want = (size_t)(content_length - received); + if (want > sizeof(buf)) { + want = sizeof(buf); + } + int got = c.read(buf, want); + if (got <= 0) { + continue; + } + + if (fs_write(out, buf, got) != got) { + Serial.println("Filesystem write failed"); + c.stop(); + return -1; + } + received += got; + last = millis(); + } + + c.stop(); + return received == content_length ? received : -1; +} + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("Full OTA update (loader + sketch) over WiFi"); + + Serial.print("Connecting to "); + Serial.println(ssid); + while (WiFi.begin(ssid, pass) != WL_CONNECTED) { + Serial.println("Connection failed, retrying..."); + delay(5000); + } + + Serial.print("IP: "); + Serial.println(WiFi.localIP()); + + fs_unlink(STAGING_PATH); + + struct fs_file_t file; + fs_file_t_init(&file); + if (fs_open(&file, STAGING_PATH, FS_O_CREATE | FS_O_WRITE) < 0) { + Serial.println("Cannot open staging file"); + return; + } + + Serial.print("Downloading http://"); + Serial.print(server); + Serial.print(":"); + Serial.print(port); + Serial.println(path); + + long len = httpGet(client, server, port, path, &file); + fs_close(&file); + + if (len < 0) { + Serial.println("Download failed"); + fs_unlink(STAGING_PATH); + return; + } + + Serial.print("Downloaded "); + Serial.print(len); + Serial.println(" bytes"); + + if (ota_loader_ready() < 0) { + Serial.println("Failed to mark update as ready"); + return; + } + + Serial.println("Rebooting..."); + Serial.flush(); + delay(100); + + ota_loader_start(); +} + +void loop() { + delay(1000); +} diff --git a/cores/arduino/ota_api.h b/libraries/OTA/examples/OTAFullWiFi/arduino_secrets.h similarity index 59% rename from cores/arduino/ota_api.h rename to libraries/OTA/examples/OTAFullWiFi/arduino_secrets.h index e97bf3460..5cbee2c48 100644 --- a/cores/arduino/ota_api.h +++ b/libraries/OTA/examples/OTAFullWiFi/arduino_secrets.h @@ -4,8 +4,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -#ifdef CONFIG_OTA - -#include "../../loader/ota/ota_api.h" - -#endif // CONFIG_OTA +#define SECRET_SSID "" +#define SECRET_PASS "" diff --git a/libraries/OTA/examples/OTAWiFi/OTAWiFi.ino b/libraries/OTA/examples/OTAWiFi/OTAWiFi.ino new file mode 100644 index 000000000..6690a3d39 --- /dev/null +++ b/libraries/OTA/examples/OTAWiFi/OTAWiFi.ino @@ -0,0 +1,192 @@ +/* + * OTA sketch update over WiFi. + * + * Downloads a raw .bin sketch image over plain HTTP and installs it on + * the next boot. No .ota container — the server hosts the plain + * ".elf-zsk.bin" artifact produced by the build. Serve it with: + * + * python3 -m http.server 8000 + * + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#ifndef CONFIG_OTA +#error "This variant's loader was built without CONFIG_OTA." +#endif + +#define STAGING_PATH CONFIG_OTA_SKETCH_UPDATE_PATH CONFIG_OTA_SKETCH_TEMP_PATH_POSTFIX + +#include "arduino_secrets.h" +char ssid[] = SECRET_SSID; +char pass[] = SECRET_PASS; + +const char *server = "10.10.10.10"; +const uint16_t port = 8000; +const char *path = "/UPDATE.bin"; + +ZephyrClient client; + +static long httpGet(Client &c, const char *host, uint16_t port, + const char *path, struct fs_file_t *out) { + if (!c.connect(host, port)) { + Serial.println("Connection failed"); + return -1; + } + + c.print("GET "); + c.print(path); + c.print(" HTTP/1.1\r\nHost: "); + c.print(host); + c.print("\r\nConnection: close\r\n\r\n"); + + char line[128]; + bool first = true; + long content_length = -1; + uint32_t last = millis(); + + while (true) { + if (!c.available()) { + if (!c.connected() || millis() - last > 10000) { + c.stop(); + return -1; + } + continue; + } + last = millis(); + + size_t n = c.readBytesUntil('\n', line, sizeof(line) - 1); + line[n] = '\0'; + if (n > 0 && line[n - 1] == '\r') { + line[n - 1] = '\0'; + } + + if (first) { + if (strncmp(line, "HTTP/1.", 7) != 0 || atoi(line + 9) != 200) { + Serial.print("HTTP error: "); + Serial.println(line); + c.stop(); + return -1; + } + first = false; + continue; + } + + if (line[0] == '\0') { + break; + } + + if (strncasecmp(line, "Content-Length:", 15) == 0) { + content_length = atol(line + 15); + } + } + + if (content_length <= 0) { + Serial.println("Missing or zero Content-Length"); + c.stop(); + return -1; + } + + uint8_t buf[512]; + long received = 0; + last = millis(); + + while (received < content_length) { + int avail = c.available(); + if (avail <= 0) { + if (!c.connected() || millis() - last > 10000) { + break; + } + continue; + } + + size_t want = (size_t)(content_length - received); + if (want > sizeof(buf)) { + want = sizeof(buf); + } + int got = c.read(buf, want); + if (got <= 0) { + continue; + } + + if (fs_write(out, buf, got) != got) { + Serial.println("Filesystem write failed"); + c.stop(); + return -1; + } + received += got; + last = millis(); + } + + c.stop(); + return received == content_length ? received : -1; +} + +void setup() { + Serial.begin(115200); + while (!Serial) { + delay(10); + } + + Serial.println("OTA update over WiFi"); + + Serial.print("Connecting to "); + Serial.println(ssid); + while (WiFi.begin(ssid, pass) != WL_CONNECTED) { + Serial.println("Connection failed, retrying..."); + delay(5000); + } + + Serial.print("IP: "); + Serial.println(WiFi.localIP()); + + fs_unlink(STAGING_PATH); + + struct fs_file_t file; + fs_file_t_init(&file); + if (fs_open(&file, STAGING_PATH, FS_O_CREATE | FS_O_WRITE) < 0) { + Serial.println("Cannot open staging file"); + return; + } + + Serial.print("Downloading http://"); + Serial.print(server); + Serial.print(":"); + Serial.print(port); + Serial.println(path); + + long len = httpGet(client, server, port, path, &file); + fs_close(&file); + + if (len < 0) { + Serial.println("Download failed"); + fs_unlink(STAGING_PATH); + return; + } + + Serial.print("Downloaded "); + Serial.print(len); + Serial.println(" bytes"); + + if (ota_sketch_ready() < 0) { + Serial.println("Failed to mark update as ready"); + return; + } + + Serial.println("Rebooting..."); + Serial.flush(); + delay(100); + + ota_sketch_start(); +} + +void loop() { + delay(1000); +} diff --git a/libraries/OTA/examples/OTAWiFi/arduino_secrets.h b/libraries/OTA/examples/OTAWiFi/arduino_secrets.h new file mode 100644 index 000000000..5cbee2c48 --- /dev/null +++ b/libraries/OTA/examples/OTAWiFi/arduino_secrets.h @@ -0,0 +1,8 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define SECRET_SSID "" +#define SECRET_PASS "" diff --git a/libraries/OTA/library.properties b/libraries/OTA/library.properties new file mode 100644 index 000000000..0e2a74af4 --- /dev/null +++ b/libraries/OTA/library.properties @@ -0,0 +1,9 @@ +name=OTA +version=1.0.0 +author=Arduino +maintainer=Arduino +sentence=Download and install a sketch update over the network. +paragraph=Examples that download a raw sketch binary over HTTP and ask the loader to install it on the next boot. Works with both WiFi and Ethernet. +category=Data Storage +url=https://github.com/arduino/ArduinoCore-zephyr/tree/main/libraries/OTA +architectures=zephyr_main diff --git a/libraries/OTA/src/ota.h b/libraries/OTA/src/ota.h new file mode 100644 index 000000000..c1dfb1013 --- /dev/null +++ b/libraries/OTA/src/ota.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * This function is used to make a sketch ota file ready to be applied on the next reboot. + * Under the hood this function currently renames the ota file to what the loader is going to be + * looking for. This avoids unwanted ota starting when the ota file is not ready yet + */ +int ota_sketch_ready(void); + +/* + * This function is used to trigger an ota procedure for the sketch. You need to mark the sketch as + * ready with `ota_sketch_ready` in order for it to be applied. + */ +int ota_sketch_start(void); + +/* + * This function is used to make a loader ota file ready to be applied on the next reboot. + * Under the hood this function currently renames the ota file to what the bootloader is going to be + * looking for. This avoids unwanted ota starting when the ota file is not ready yet + */ +int ota_loader_ready(void); + +/* + * This function is used to trigger an ota procedure for the loader. You need to mark the loader as + * ready with `ota_loader_ready` in order for it to be applied. + */ +int ota_loader_start(void); + +#ifdef __cplusplus +} +#endif diff --git a/loader/ota/CMakeLists.txt b/loader/ota/CMakeLists.txt index 1a7514086..83ae0beb7 100644 --- a/loader/ota/CMakeLists.txt +++ b/loader/ota/CMakeLists.txt @@ -5,6 +5,7 @@ zephyr_interface_library_named(ota) target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_sketch_check.c) target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_llext_exports.c) zephyr_include_directories(${CMAKE_CURRENT_LIST_DIR}) +zephyr_include_directories(${CMAKE_CURRENT_LIST_DIR}/../../libraries/OTA/src) target_sources_ifdef(CONFIG_SOC_SERIES_STM32H7X app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_arduino_stm32h7.c) diff --git a/loader/ota/ota_api.h b/loader/ota/ota_api.h deleted file mode 100644 index d84ecc029..000000000 --- a/loader/ota/ota_api.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) Arduino s.r.l. and/or its affiliated companies - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define OTA_SKETCH_FILENAME CONFIG_OTA_SKETCH_UPDATE_PATH CONFIG_OTA_SKETCH_TEMP_PATH_POSTFIX -#define OTA_LOADER_FILENAME CONFIG_OTA_LOADER_UPDATE_PATH CONFIG_OTA_LOADER_TEMP_PATH_POSTFIX - -int ota_sketch_ready(); -int ota_sketch_start(); - -int ota_loader_ready(); -int ota_loader_start(); - -#ifdef __cplusplus -} -#endif diff --git a/loader/ota/ota_arduino_stm32h7.c b/loader/ota/ota_arduino_stm32h7.c index 739ae02af..119181cf7 100644 --- a/loader/ota/ota_arduino_stm32h7.c +++ b/loader/ota/ota_arduino_stm32h7.c @@ -4,8 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "ota_api.h" +#include #include + +#define OTA_LOADER_FILENAME CONFIG_OTA_LOADER_UPDATE_PATH CONFIG_OTA_LOADER_TEMP_PATH_POSTFIX #include #include #include diff --git a/loader/ota/ota_llext_exports.c b/loader/ota/ota_llext_exports.c index 39947fc4e..73626b358 100644 --- a/loader/ota/ota_llext_exports.c +++ b/loader/ota/ota_llext_exports.c @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "ota_api.h" +#include #include EXPORT_SYMBOL(ota_sketch_ready); diff --git a/loader/ota/ota_sketch.c b/loader/ota/ota_sketch.c index 6014b737e..5e8cae469 100644 --- a/loader/ota/ota_sketch.c +++ b/loader/ota/ota_sketch.c @@ -7,11 +7,13 @@ #include #include -#include "ota_api.h" +#include #include LOG_MODULE_REGISTER(ota_sketch, CONFIG_OTA_LOG_LEVEL); +#define OTA_SKETCH_FILENAME CONFIG_OTA_SKETCH_UPDATE_PATH CONFIG_OTA_SKETCH_TEMP_PATH_POSTFIX + int ota_sketch_ready() { struct fs_dirent entry; int ret; diff --git a/variants/arduino_giga_r1_stm32h747xx_m7/test_setup.sh b/variants/arduino_giga_r1_stm32h747xx_m7/test_setup.sh index 6d6f58778..9f7decdf1 100644 --- a/variants/arduino_giga_r1_stm32h747xx_m7/test_setup.sh +++ b/variants/arduino_giga_r1_stm32h747xx_m7/test_setup.sh @@ -22,4 +22,6 @@ get_branch_tip libraries arduino-libraries/Arduino_Video main \ examples/LVGLDemo \ skip_for_this_board libraries/Ethernet +skip_for_this_board libraries/OTA/examples/OTAEthernet +skip_for_this_board libraries/OTA/examples/OTAFullEthernet skip_for_this_board libraries/PDM diff --git a/variants/arduino_nano_33_ble_nrf52840_sense/test_setup.sh b/variants/arduino_nano_33_ble_nrf52840_sense/test_setup.sh index 2026383af..5666e40b9 100644 --- a/variants/arduino_nano_33_ble_nrf52840_sense/test_setup.sh +++ b/variants/arduino_nano_33_ble_nrf52840_sense/test_setup.sh @@ -27,6 +27,7 @@ get_branch_tip libraries arduino-libraries/Arduino_HS300x master \ skip_for_this_board libraries/Arduino_SecureElement skip_for_this_board libraries/Camera skip_for_this_board libraries/Ethernet +skip_for_this_board libraries/OTA skip_for_this_board libraries/Storage skip_for_this_board libraries/WiFi skip_for_this_board libraries/Zephyr_SDRAM diff --git a/variants/arduino_nano_connect_rp2040/test_setup.sh b/variants/arduino_nano_connect_rp2040/test_setup.sh index 10d63f16f..db04a0cab 100644 --- a/variants/arduino_nano_connect_rp2040/test_setup.sh +++ b/variants/arduino_nano_connect_rp2040/test_setup.sh @@ -13,6 +13,7 @@ skip_for_this_board libraries/Camera skip_for_this_board libraries/Ethernet +skip_for_this_board libraries/OTA skip_for_this_board libraries/Storage skip_for_this_board libraries/WiFi skip_for_this_board libraries/Zephyr_SDRAM diff --git a/variants/arduino_nano_matter_mgm240sd22vna/test_setup.sh b/variants/arduino_nano_matter_mgm240sd22vna/test_setup.sh index 30d26fa3d..220131b9a 100644 --- a/variants/arduino_nano_matter_mgm240sd22vna/test_setup.sh +++ b/variants/arduino_nano_matter_mgm240sd22vna/test_setup.sh @@ -17,6 +17,7 @@ skip_for_this_board examples/arduino-examples/examples/04.Communication/SerialPa skip_for_this_board libraries/Arduino_SecureElement skip_for_this_board libraries/Camera skip_for_this_board libraries/Ethernet +skip_for_this_board libraries/OTA skip_for_this_board libraries/Storage skip_for_this_board libraries/WiFi skip_for_this_board libraries/Zephyr_SDRAM diff --git a/variants/arduino_nicla_sense_me_nrf52832/test_setup.sh b/variants/arduino_nicla_sense_me_nrf52832/test_setup.sh index 507bcefe2..a84cef147 100644 --- a/variants/arduino_nicla_sense_me_nrf52832/test_setup.sh +++ b/variants/arduino_nicla_sense_me_nrf52832/test_setup.sh @@ -18,6 +18,7 @@ skip_for_this_board libraries/Arduino_SecureElement skip_for_this_board libraries/ArduinoBLE skip_for_this_board libraries/Camera skip_for_this_board libraries/Ethernet +skip_for_this_board libraries/OTA skip_for_this_board libraries/Storage skip_for_this_board libraries/WiFi skip_for_this_board libraries/Zephyr_SDRAM diff --git a/variants/arduino_nicla_vision_stm32h747xx_m7/test_setup.sh b/variants/arduino_nicla_vision_stm32h747xx_m7/test_setup.sh index 12c6ae136..94b0cce22 100644 --- a/variants/arduino_nicla_vision_stm32h747xx_m7/test_setup.sh +++ b/variants/arduino_nicla_vision_stm32h747xx_m7/test_setup.sh @@ -12,5 +12,7 @@ # skip_for_this_board removes all tests under the given path prefix. skip_for_this_board libraries/Ethernet +skip_for_this_board libraries/OTA/examples/OTAEthernet +skip_for_this_board libraries/OTA/examples/OTAFullEthernet skip_for_this_board libraries/CAN skip_for_this_board libraries/PDM diff --git a/variants/arduino_portenta_c33_r7fa6m5bh3cfc/test_setup.sh b/variants/arduino_portenta_c33_r7fa6m5bh3cfc/test_setup.sh index f2f6e2254..00cd948fc 100644 --- a/variants/arduino_portenta_c33_r7fa6m5bh3cfc/test_setup.sh +++ b/variants/arduino_portenta_c33_r7fa6m5bh3cfc/test_setup.sh @@ -12,5 +12,6 @@ # skip_for_this_board removes all tests under the given path prefix. skip_for_this_board libraries/Camera +skip_for_this_board libraries/OTA skip_for_this_board libraries/RTC skip_for_this_board libraries/PDM