From d808d964d291c552cb20bd923b9f04c0f4d89978 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 23 Oct 2026 16:36:18 -0700 Subject: [PATCH] Add nRF52 Repeater and Room variants with embedded OTAFIX 2.4.12 --- .github/workflows/run-unit-tests.yml | 7 + build_legacy.sh | 53 +- docs/_data/bootloader_manifest.json | 743 +++++++++++------- docs/nrf52_embedded_bootloader.md | 102 +++ docs/ota_nrf52_bootloader_update.md | 4 + platformio.ini | 1 + scripts/firmware_build_recipe.py | 5 + scripts/nrf52_embedded_bootloader.py | 285 +++++++ scripts/nrf52_embedded_bootloader_pio.py | 33 + scripts/nrf52_internal_bootloader_link.py | 7 + scripts/qualify_nrf52_embedded_bootloader.py | 69 ++ src/Mesh.cpp | 17 + src/helpers/ota/BootloaderMigration.cpp | 260 ++++++ src/helpers/ota/BootloaderMigration.h | 15 + src/helpers/ota/BootloaderMigrationPolicy.h | 106 +++ src/helpers/ota/OtaCli.cpp | 20 + src/helpers/ota/OtaContext.h | 25 + test/test_firmware_picker.js | 6 +- test/test_nrf52_embedded_bootloader.py | 343 ++++++++ test/test_nrf52_embedded_variant.py | 257 ++++++ tools/mota/README.md | 5 + tools/mota/test_bootloader_migration.py | 185 +++++ .../mota/test_bootloader_migration_runtime.py | 86 ++ tools/mota/tests/migration_stubs/Arduino.h | 4 + .../BootloaderMigrationResource.h | 10 + tools/mota/tests/migration_stubs/SHA256.h | 11 + tools/mota/tests/migration_stubs/Utils.h | 10 + tools/mota/tests/migration_stubs/nrf.h | 35 + tools/mota/tests/migration_stubs/nrf_mbr.h | 15 + tools/mota/tests/migration_stubs/nrf_sdm.h | 5 + tools/mota/tests/migration_stubs/nrf_soc.h | 4 + tools/mota/tests/migration_stubs/runtime.cpp | 261 ++++++ 32 files changed, 2702 insertions(+), 287 deletions(-) create mode 100644 docs/nrf52_embedded_bootloader.md create mode 100644 scripts/nrf52_embedded_bootloader.py create mode 100644 scripts/nrf52_embedded_bootloader_pio.py create mode 100644 scripts/qualify_nrf52_embedded_bootloader.py create mode 100644 src/helpers/ota/BootloaderMigration.cpp create mode 100644 src/helpers/ota/BootloaderMigration.h create mode 100644 src/helpers/ota/BootloaderMigrationPolicy.h create mode 100644 test/test_nrf52_embedded_bootloader.py create mode 100644 test/test_nrf52_embedded_variant.py create mode 100644 tools/mota/test_bootloader_migration.py create mode 100644 tools/mota/test_bootloader_migration_runtime.py create mode 100644 tools/mota/tests/migration_stubs/Arduino.h create mode 100644 tools/mota/tests/migration_stubs/BootloaderMigrationResource.h create mode 100644 tools/mota/tests/migration_stubs/SHA256.h create mode 100644 tools/mota/tests/migration_stubs/Utils.h create mode 100644 tools/mota/tests/migration_stubs/nrf.h create mode 100644 tools/mota/tests/migration_stubs/nrf_mbr.h create mode 100644 tools/mota/tests/migration_stubs/nrf_sdm.h create mode 100644 tools/mota/tests/migration_stubs/nrf_soc.h create mode 100644 tools/mota/tests/migration_stubs/runtime.cpp diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index ad3144c42..94e571c23 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -262,6 +262,13 @@ jobs: - name: Verify bootloader version regressions without hardware or downloads run: python3 -B test/test_nrf52_bootloader_version.py -v + - name: Verify embedded bootloader variant and guarded activation + run: | + python3 -B test/test_nrf52_embedded_variant.py -v + python3 -B test/test_nrf52_embedded_bootloader.py -v + python3 -B tools/mota/test_bootloader_migration.py -v + python3 -B tools/mota/test_bootloader_migration_runtime.py -v + - name: Install signed-package verification dependency run: python3 -m pip install cryptography diff --git a/build_legacy.sh b/build_legacy.sh index be5b64f67..4f3a85844 100755 --- a/build_legacy.sh +++ b/build_legacy.sh @@ -149,6 +149,8 @@ Options: Default for Option 3; Companion USB updates remain supported. Cable-only infrastructure platforms are reported and omitted. --allow-no-ota: Include cable-only development targets instead. + --with-bootloader <2.4.12>: Build a separate nRF52 Repeater/Room variant + carrying the exact-board OTAFIX bootloader for authenticated activation. --background: Run the selected build as a persistent detached job. Its log and status live outside OUTPUT_DIR so --clean cannot erase them. @@ -1052,6 +1054,13 @@ parse_cli_options() { REQUIRE_OTA_UPDATES=1 shift ;; + --with-bootloader) + if [ "$#" -lt 2 ] || [ "$2" != "2.4.12" ]; then + echo "--with-bootloader requires the pinned release 2.4.12"; return 1 + fi + export MESHCORE_NRF52_EMBED_BOOTLOADER="$2" + shift 2 + ;; --allow-no-ota) REQUIRE_OTA_UPDATES=0 shift @@ -3480,6 +3489,9 @@ record_application_expectation() { declare_full_logging_application_contract() { local env_name=$1 BUILD_APPLICATION_EXPECTATIONS=() + if [ -n "${MESHCORE_NRF52_EMBED_BOOTLOADER:-}" ]; then + record_application_expectation "bootloader.embedded" "BL embedded 2.4.12" + fi if requires_full_usb_packet_logging "$env_name" \ || is_xiao_qspi_canonical_repeater_build "$env_name" \ || is_nrf52_paired_runtime_logging_build "$env_name"; then @@ -4671,7 +4683,8 @@ write_build_capability_manifest() { local env_platform=$2 local pio_env_name=$3 local firmware_filename=$4 - local artifact_target=${FIRMWARE_OUTPUT_ENV_NAME:-$env_name} + local artifact_target + artifact_target=$(embedded_bootloader_artifact_target "${FIRMWARE_OUTPUT_ENV_NAME:-$env_name}") || return 1 if [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ] \ && is_nrf52_sensor_ota_pair_target "$env_name"; then artifact_target+="-${NRF52_OTA_SENSOR_PROFILE}-ota" @@ -4812,7 +4825,7 @@ output_artifact_exists() { compute_build_recipe_digest() { local env_name=$1 env_platform=$2 pio_env_name=$3 embedded_version=$4 local original_flags=$5 full_source_commit=$6 artifact_target checkout_dir - artifact_target=${FIRMWARE_OUTPUT_ENV_NAME:-$env_name} + artifact_target=$(embedded_bootloader_artifact_target "${FIRMWARE_OUTPUT_ENV_NAME:-$env_name}") || return 1 if [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ] \ && is_nrf52_sensor_ota_pair_target "$env_name"; then artifact_target+="-${NRF52_OTA_SENSOR_PROFILE}-ota" @@ -4883,7 +4896,7 @@ build_artifacts_exist() { nrf52_sensor_profile_manifest_matches \ "${OUTPUT_DIR}/${firmware_filename}.capabilities.json" "$env_name" \ "$NRF52_OTA_SENSOR_PROFILE" \ - "${FIRMWARE_OUTPUT_ENV_NAME:-$env_name}-${NRF52_OTA_SENSOR_PROFILE}-ota" || return 1 + "$(embedded_bootloader_artifact_target "${FIRMWARE_OUTPUT_ENV_NAME:-$env_name}")-${NRF52_OTA_SENSOR_PROFILE}-ota" || return 1 fi # Old manifests can be marked verified while lacking a newly required # capability. Never let --resume bypass the current packaged-logging gate. @@ -4976,14 +4989,30 @@ collect_build_artifacts() { python3 scripts/package_pocket_gui_license.py collect "$build_output_dir" \ --stem "${OUTPUT_DIR}/${firmware_filename}" || return $? + if [ -n "${MESHCORE_NRF52_EMBED_BOOTLOADER:-}" ]; then + python3 scripts/qualify_nrf52_embedded_bootloader.py "$build_output_dir" \ + "${OUTPUT_DIR}/${firmware_filename}.capabilities.json" || return $? + fi + python3 scripts/firmware_memory_manifest.py package "$build_output_dir" \ --stem "${OUTPUT_DIR}/${firmware_filename}" || return $? } +embedded_bootloader_artifact_target() { + if [ -n "${MESHCORE_NRF52_EMBED_BOOTLOADER:-}" ]; then + [ "$MESHCORE_NRF52_EMBED_BOOTLOADER" = "2.4.12" ] || return 1 + echo "${1}-with-bootloader-${MESHCORE_NRF52_EMBED_BOOTLOADER}" + else + echo "$1" + fi +} + get_firmware_filename() { local env_name=$1 local firmware_version_string=$2 local filename_infix=$FIRMWARE_FILENAME_INFIX + local artifact_env_name + artifact_env_name=$(embedded_bootloader_artifact_target "$env_name") || return 1 # Make LoRa-OTA artifacts obvious without changing the PlatformIO environment # name or the stable MOTA target identity. FULL artifacts retain their @@ -5001,9 +5030,9 @@ get_firmware_filename() { fi if [ -n "$filename_infix" ]; then - echo "${env_name}-${filename_infix}-${firmware_version_string}" + echo "${artifact_env_name}-${filename_infix}-${firmware_version_string}" else - echo "${env_name}-${firmware_version_string}" + echo "${artifact_env_name}-${firmware_version_string}" fi } @@ -5146,6 +5175,14 @@ build_firmware_one_profile() { echo "Unsupported or unknown platform for env: $env_name" return 1 fi + if [ -n "${MESHCORE_NRF52_EMBED_BOOTLOADER:-}" ]; then + if [ "$MESHCORE_NRF52_EMBED_BOOTLOADER" != "2.4.12" ] \ + || [ "$env_platform" != "NRF52_PLATFORM" ] \ + || ! { is_repeater_role_target "$env_name" || is_room_server_role_target "$env_name"; }; then + echo "Embedded bootloader variants require an nRF52 Repeater or Room and release 2.4.12." >&2 + return 1 + fi + fi pio_env_name=$(get_pio_build_env "$env_name") if { [ "$COMPLETE_OTA_FIRST_PASS" = "1" ] \ || [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ]; } \ @@ -5379,6 +5416,12 @@ build_firmware_one_profile() { append_platformio_extra_script "post:scripts/check_firmware_ram.py" fi + if [ -n "${MESHCORE_NRF52_EMBED_BOOTLOADER:-}" ] \ + && ! pio_env_option_contains "$pio_env_name" extra_scripts \ + "scripts/nrf52_embedded_bootloader_pio.py"; then + append_platformio_extra_script "pre:scripts/nrf52_embedded_bootloader_pio.py" + fi + recipe_build_flags=$PLATFORMIO_BUILD_FLAGS BUILD_RECIPE_SHA256=$(compute_build_recipe_digest "$env_name" "$env_platform" \ "$pio_env_name" "$embedded_version_string" "$original_platformio_build_flags" \ diff --git a/docs/_data/bootloader_manifest.json b/docs/_data/bootloader_manifest.json index 80eeadfd2..e85b37821 100644 --- a/docs/_data/bootloader_manifest.json +++ b/docs/_data/bootloader_manifest.json @@ -1,9 +1,9 @@ { "schemaVersion": 1, "repository": "mikecarper/Adafruit_nRF52_Bootloader_OTAFIX", - "tag": "v0.11.0-OTAFIX2.4.11", - "version": "2.4.11", - "releaseUrl": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.11", + "tag": "v0.11.0-OTAFIX2.4.12", + "version": "2.4.12", + "releaseUrl": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.12", "prerelease": false, "profiles": [ { @@ -18,22 +18,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-gat562_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-gat562_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 84992, - "sha256": "2e3c5292abd9f87380e63926a01dc0cb437002d33acff8de2919d9cd79273927" + "name": "update-gat562_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-gat562_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 83968, + "sha256": "6ca09f6c4debdae97b67f3ea50293f6e803d999a4a7a04870ae55777e17bf21d" }, "zip": { - "name": "gat562_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/gat562_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "gat562_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/gat562_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "35bbb66831af4c1aaba1ae139f2c7d149a1b281c28aff3dac31a78f67ac026b0" + "sha256": "86490cbca323a8b81fc8490f9a43fd06db4c776bac6aa0bda91ee807cf801c60" }, "hex": { - "name": "gat562_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/gat562_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 530716, - "sha256": "8df22a6951916f0eee1910cf8d449eb32fa712799ad12713045e06f93e355f82" + "name": "gat562_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/gat562_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 529264, + "sha256": "0b6bb52d2eac5edb6aa9f303ff9526a45002ecfffc559f05b10f34eec3c676a4" } } }, @@ -47,22 +47,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 84480, - "sha256": "3087bdd540db6a6399120a23012dc9d1e45ca9b72938abd49c389319f1a027db" + "name": "update-heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 83456, + "sha256": "dff59d9dc609700b2d05d1e1a625eba42ffa878882bce17296c4753442d92489" }, "zip": { - "name": "heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "7251ed15ce18dba03151b676d7401f50a043a61370e6eb7230daddfa9248dda4" + "sha256": "65f4a62e02027f7852a04dcdd7624f6be467ca3fc4073157201043fb7ceebc51" }, "hex": { - "name": "heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 530112, - "sha256": "f0b836e53af97d5445236ef89adb8443e5c8a4691e71488ff930cca2b3a57d90" + "name": "heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/heltec_mesh_pocket_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 528704, + "sha256": "648c736cf28454a241028f1546fa19e33aa87ff5d3d6346700eb93bd92a45624" } } }, @@ -76,22 +76,22 @@ "storage": "sd", "files": { "uf2": { - "name": "update-heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 88064, - "sha256": "26e8cc90bdf3a138bd8f5471f17c04cbeaddf5dfb01bf62601917f7d0c8fa825" + "name": "update-heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 87552, + "sha256": "a82eca7a1287de26c0938c97039ac6349816aa585c57f199ddbcb5f8c77b751d" }, "zip": { - "name": "heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "size": 192833, - "sha256": "ac5fdfa777ba583cc9e62e7cd1fef3cf0a3b97da5b221a332380b2209d31b79c" + "name": "heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "size": 192834, + "sha256": "1b8beb90f26fb331e89a3f7ad4d07df130126fcef558031755490172dd94f6ea" }, "hex": { - "name": "heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 535746, - "sha256": "5980d8f96a3b9dbb55bbf488cbeacdcae1c6f59ff78c2766254c8b95c5e6beab" + "name": "heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/heltec_mesh_tower_v2_sdcard_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 534562, + "sha256": "3e177b262d8378a2f4153e8b3e23a0a202626f5a5114e35e2bc162a21f8dd1b0" } } }, @@ -105,22 +105,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 87552, - "sha256": "a7db3f390ab8934a33ccc1f60ac0acf1b5200f58fc6c037e233a178d5d416da8" + "name": "update-heltec_t096_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-heltec_t096_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 86528, + "sha256": "dc39e0361b3e572a7f546695cb44156c6a051ed7783872890dc97dbc608be609" }, "zip": { - "name": "heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "size": 192834, - "sha256": "4c2f1adfa28ad8a4a856c7c720ad1dbcb8cf13fcdc381152761bd72fadba227c" + "name": "heltec_t096_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/heltec_t096_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "size": 192833, + "sha256": "5d7121eb3fc9b429b0bfc03a46cd39c49cea99a523ef252fca76fbe83ae9c083" }, "hex": { - "name": "heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t096_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 534470, - "sha256": "878071c4e7bc16a9ec664773d34eadb7be6846e8791bcc87270ba5e41b463e2b" + "name": "heltec_t096_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/heltec_t096_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 533016, + "sha256": "5b51abde8a8ed62264f2a87c2ffdf1b9b9087c64b039da1f33cf64b28e363735" } } }, @@ -134,22 +134,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 87552, - "sha256": "3a053de7ba28e63756cd52d0e5632ce07bd244eeb271720f1178e71178680d77" + "name": "update-heltec_t1_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-heltec_t1_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 86528, + "sha256": "3d78354c72a22382b9c28a3aa1046b751667c34b00f1e16993f3f2ba33e12d3e" }, "zip": { - "name": "heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "size": 192834, - "sha256": "b7144a4493a23902b07e31435f8c3758afe0b5d87f777236705865f51bb00ecb" + "name": "heltec_t1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/heltec_t1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "size": 192833, + "sha256": "ae6904dac781d1bea7fe0547abab3e95ea116114d3c57645084ef6ca91bbc444" }, "hex": { - "name": "heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 534508, - "sha256": "8ba4ddc312f2246e5ace749e5734934ab5537c4ef60354a34658eebfa4d50449" + "name": "heltec_t1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/heltec_t1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 533054, + "sha256": "157d3180eeac0992878cabdc8e46fdc0fab1061d7687c9a6d9530bb26b41e04b" } } }, @@ -164,22 +164,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 88064, - "sha256": "d8307134c4f63a9a91d86937abe25863722ff61f822524fc027847c6137282cc" + "name": "update-heltec_t114_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-heltec_t114_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 87040, + "sha256": "2bbbd2bcb0264e2558fa8b393faba250f35eb5fb4df2785a94857519a50a436b" }, "zip": { - "name": "heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "heltec_t114_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/heltec_t114_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "a473faf32cf3e4dc13c5d8340e7ffebd04741be3e37f8c2cb98b2c176e01a777" + "sha256": "138c69fe816c794f1effde6ddcee768dae4aa0e3bfeff90e3635ac3a182a3f0a" }, "hex": { - "name": "heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/heltec_t114_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 535380, - "sha256": "5afa51d860b54dc820d94f67efb8f7f449b0a4f461bbc176ccbd27a9730bc4e9" + "name": "heltec_t114_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/heltec_t114_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 534148, + "sha256": "da5c7968f6c2c90f9c578acb95f04700f59b1a7e9365d2b045c5ee3dd9c16425" } } }, @@ -193,22 +193,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 84480, - "sha256": "c2ad478f12cd3176f5ea4442726fdaebb6071218564a97b7c90cf52ed214f369" + "name": "update-keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 83456, + "sha256": "15ffb2c260fc1e07bb06b5718ee571ea125e82653fbecd882459ab09c42b87e1" }, "zip": { - "name": "keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "f7a21a21afad7ecbf0e29f65e243b103f4b423fe575734fa4f00db56947f7c64" + "sha256": "4d1f29efaf156e388262a1a603ea72e4ee98ff7547e26b71cb0a93ad732864b2" }, "hex": { - "name": "keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 530034, - "sha256": "dbad71f1975f26c4f42e3057d2e6de82201fa9488e39556070ad68fa650204fe" + "name": "keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/keepteen_lt1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 528626, + "sha256": "95e5854b86a028d4252a42d6819664fcfd7fedc20917318b06663cf19000f982" } } }, @@ -222,22 +222,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 86528, - "sha256": "9a3cd2a73f91f9f1ca01f44ba56c4b1ad2317511e766533ed61c361aca6e37fa" + "name": "update-lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 85504, + "sha256": "aff3b36baea7567bfc8fc83580ede11d27d9e450b6b4c8b0cd039e66eaa2fa5d" }, "zip": { - "name": "lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "a7cbdf2c911542222c0d12eb5fb2ab9bdf2263495b591d1a946860b324bf4731" + "sha256": "55d935ce7fb4d7c3944f93b38240f311255d6a0c8560b0783d7d0121e72098ac" }, "hex": { - "name": "lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 532930, - "sha256": "5c8f10b9b1217f36b8ff6f0dac957f108ca4125bc5e737dd827c08c01a5e556f" + "name": "lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/lilygo_techo_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 531654, + "sha256": "ba5428bbdf61b9bfd0cf0697d5175cfd995b7ff272725a895e8b4a7866756ffc" } } }, @@ -252,22 +252,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 87040, - "sha256": "5ee193ed3ac342502efb03adc3aa214eb56198f22975fe5dfcf4d5743d4788d0" + "name": "update-lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 86016, + "sha256": "8e7636a43b9eeb8561a51eb161732e0ee20b0e6a051bd9d12e87387aa4605758" }, "zip": { - "name": "lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "2ed15158b2b015a445c3d91ad616c8550e3b6ed59de2ca54832de4cb8d5ce10d" + "sha256": "cde601f61d858116ff591488e1856db747a77cdc8e3c3025e588cb79463ac440" }, "hex": { - "name": "lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 533526, - "sha256": "d3531513bc550431caf9893dd88a8324f83f401b7b1700bbee6cd6c8a2724983" + "name": "lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/lilygo_techo_lite_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 532250, + "sha256": "557c21225400f078964b061f90827563baa42ae0be3494699af50ffd98bd646b" } } }, @@ -281,22 +281,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 84480, - "sha256": "c5858e6ed516af22202a2c9b1c155764ae25adaf55a42941d9665df77d44fad7" + "name": "update-minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 83456, + "sha256": "8461d1fac82a70ccb02591964bf107fb7925f50408351f3550eea110a69a26f8" }, "zip": { - "name": "minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "name": "minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", "size": 194546, - "sha256": "322b8c8e9caaca660bd2ed68f1d19f5cd38575ea7def6f262e652e2a872f7f2d" + "sha256": "4d7ca3a59358cfdfbddbaac2d5f7f99dae346469dfbcc9ed6dad56ca72646552" }, "hex": { - "name": "minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "size": 535084, - "sha256": "9de94798b5c25018ee9a5494b776bd5469f5c743d0fa9cbb82dfc7dda115fbbf" + "name": "minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/minewsemi_mx25le01_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "size": 533676, + "sha256": "ecbfc02399767fe7d8b4a227eed75aafcca92bb3d04ba3122cb7307b6a81fa8a" } } }, @@ -310,22 +310,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-pca10056_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-pca10056_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 86528, - "sha256": "78ab8f3f8ea44d60c13d6fd34088451b1bd26745757deed44826c15c7ee70ea8" + "name": "update-pca10056_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-pca10056_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 85504, + "sha256": "2d6328dc284f1b852eab7b66236f7dfc30dbaeecc375968d0ff38a5cfed26b75" }, "zip": { - "name": "pca10056_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/pca10056_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "pca10056_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/pca10056_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "d5d01a01f98850b3282cf7f443fa13e642a42617d7267cee22739880fbcf2ff9" + "sha256": "b77d8b93eb831727f7676b368703110f2d487a02e9c5ec3ce6449a9351aca0c0" }, "hex": { - "name": "pca10056_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/pca10056_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 533020, - "sha256": "51ceaa88711ca24a8430bb6f35766df729245cbe2858336dad3082751c692fc2" + "name": "pca10056_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/pca10056_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 531744, + "sha256": "b36872f56bfa85b47d6469ff5f248e4e1f514e53004b0b73348b48cd0cd09e29" } } }, @@ -339,22 +339,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 84480, - "sha256": "b849203d30a56b5243f8d92e308c6cd9a1eb3b35459072204f9f044d74dbad09" + "name": "update-promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 83456, + "sha256": "de2abb8be577085432b7156c7ae29d02d080f1a24c110b5a91a0655991546a50" }, "zip": { - "name": "promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "3b2ef682cc2641626b4db3d8bfe821cd86dd701f38c7c0c055864d78da0fbb88" + "sha256": "03bcaee48da4717afb34a4eda2f1161e694b7a974189988c142969ff042e39bb" }, "hex": { - "name": "promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 530426, - "sha256": "bcfda096684f5da9f00c36a0baef81a0b4655424f594cc29953c27ca9051263f" + "name": "promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/promicro_nrf52840_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 529018, + "sha256": "02afe1640563b44ef9a3ce40f92d5346be436cd27a349a5a0f0da64f77b1d03c" } } }, @@ -368,22 +368,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 86016, - "sha256": "ca98f27f0289189d0b9b600c5a5a82acad14a56ed4ee4922c6c17fe1d669aadf" + "name": "update-sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 85504, + "sha256": "84c378bd47d06e4cdf964b959c3629329221faac4765b6a5edb22191373e9a03" }, "zip": { - "name": "sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "name": "sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", "size": 194546, - "sha256": "2ed90c5624a3cdb9852fb22da42f02c2d55d93e74e19978068570c1edce90ee8" + "sha256": "694fca8e68fb695ac1a5ef701baf26e8d52a7eb188f886496dcb6bc8b6fdc7cb" }, "hex": { - "name": "sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "size": 537380, - "sha256": "6a0ada53219f67dd1a99da32d1257acff5fa617db26a9c3931dfa69e64bfca96" + "name": "sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/sensecap_solar_p1_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "size": 536148, + "sha256": "9b949347259a97da32f893de16af9ecba55563092c37ece2684f00a53a4dfc5c" } } }, @@ -397,22 +397,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 84480, - "sha256": "56a66098af73f8ef71d7709fb2e4803f0822b146310882cfccd9cd67dc7e6f8a" + "name": "update-t1000_e_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-t1000_e_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 83456, + "sha256": "0299f9bdad4f54c07e514661d4286192ce906e5a4de748cf92557a2085d5a75c" }, "zip": { - "name": "t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "name": "t1000_e_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/t1000_e_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", "size": 194546, - "sha256": "25e4f5af918ee1db913eb66f867f601578267ca5ebd8d4ebe7f6a43e193992e0" + "sha256": "35b2d7a3a45a2223da48c30a66f187ade82f63439457c8680a1732ad5dbbfc73" }, "hex": { - "name": "t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/t1000_e_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "size": 534936, - "sha256": "9d75645dcf05a3f026ae2e798294dfbdb256288853fa18a8fc3d81a36df2079f" + "name": "t1000_e_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/t1000_e_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "size": 533704, + "sha256": "7cdbd271ac4c0001da0e6fb345f8c4f75e96b5b85abefad077623afa2848430d" } } }, @@ -426,22 +426,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 86016, - "sha256": "e9285e60f5789380f5aa7c61773e3d4d5dc61054688ec4e83830d3ea118f89d3" + "name": "update-thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 85504, + "sha256": "5f82e0b1f9b8dc4fd512213f8c6276210f4faab2f2e1c8572bdd525e059962aa" }, "zip": { - "name": "thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "size": 192833, - "sha256": "3cac3e8f3b73a4e46e99fd11502954dd82e416274f413019d79265d8bca4e4b0" + "name": "thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "size": 192834, + "sha256": "39758d00c22b010f64fb7c757ac0a600cce5fb2cfb7024879eda5e6ca2a5ed7d" }, "hex": { - "name": "thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 532658, - "sha256": "2c42f4334ac94e05cce770fe9570b9442f5d37c54fb3c42d3e9f3caf2d5f36d1" + "name": "thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/thinknode_m1_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 531426, + "sha256": "1580b819ccb8a918686e264d5d434694a53fb2e195bb03feea4dafeeac9b6396" } } }, @@ -455,22 +455,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 84480, - "sha256": "babdc56164774911c53c4c0384ff12a110b07a5afc4998ee2ada021808a15a28" + "name": "update-thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 83456, + "sha256": "ab4d4e7da60da78383950ecdd8932635468937eb90354040b0ed1fd8430c0f49" }, "zip": { - "name": "thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "d5997d7c3cd0917b5fc0302a50eb0543897b5009371e5400c728901002d8ef04" + "sha256": "26b86db8840e1de3f309a20ccc7e18fedd820dc645c5c85669bd9e0d1c029cb6" }, "hex": { - "name": "thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 530140, - "sha256": "25fe01c8b9e2b73d9b8723e538dbeda48e136142884bb1769a527b4c183359db" + "name": "thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/thinknode_m3_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 528688, + "sha256": "07494cbb9c596b9dd1303d02600bdf31b50586e1a0f191401e9220dc81990752" } } }, @@ -484,22 +484,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 86016, - "sha256": "c177e30bebd37a5a495780ebe472eaa49e50294f96cfb836568bce03dfc69d32" + "name": "update-thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 85504, + "sha256": "de82c05cbd77100b23ae5d9c917eea46eaf4cd4115ede9557046377dc580172b" }, "zip": { - "name": "thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "4615d333486d6638fe7b10f0ed82b2bb4ba25424bf11b1662a35f117d6f4060a" + "sha256": "950475dcbf3e4985b6754737527261489d83614246f1a6ecd91f2148b135a4dc" }, "hex": { - "name": "thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 532746, - "sha256": "5a6c937a222d2434a59b63eb7369a392f018404dbfc71913a27c48c64042fbec" + "name": "thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/thinknode_m6_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 531470, + "sha256": "3a070a5f817cc369e75eb354120aba5ad5f409a8b1a03b1f42bed277b9e3815b" } } }, @@ -515,22 +515,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 86016, - "sha256": "94270e321b602086b9b00a3a80d5f9cec07d16bd095efc252f2d6ddf4086adb9" + "name": "update-wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 84992, + "sha256": "2c8aac42365f6ae1d0a137531fd7ef8f28f872ff046be3c2fced015b26ab7945" }, "zip": { - "name": "wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", - "size": 194545, - "sha256": "1d6436b2c4c434f5f5a0cb8646c97a3087afaf43cc79403da78ed85b36ea0313" + "name": "wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", + "size": 194546, + "sha256": "aceae36b3b4c8fa2fa72be9a0d68d63bed53398a17ace6736e932b99b6d85b58" }, "hex": { - "name": "wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "size": 537290, - "sha256": "4513b435762d69db75fc233a099d21a02efe6481d75f38dd37a64aa5da5116a6" + "name": "wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/wio_tracker_l1_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "size": 536014, + "sha256": "c46e132f4022c0bce1786867057ae61bc79db78f1fea629eb8478ee439c67f8f" } } }, @@ -544,22 +544,22 @@ "storage": "adaptive", "files": { "uf2": { - "name": "update-wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 88064, - "sha256": "e26087a540b235b5968d7e5fee7e9c8f6dcff8480ad7ea922792e88307335b67" + "name": "update-wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 87040, + "sha256": "18bb4839df8bffa87003b80b9c256b7103ac5aff4e7fa391e5ae48de1b05ee94" }, "zip": { - "name": "wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "4971a54970011c2df80ece58d917817de0d157e3fcb1a8490d1fa1f65e279db7" + "sha256": "5b2d27a7ffb3efcdd2b3e7435f7d79f5c5088817d024de3d952d7ea3a27002c9" }, "hex": { - "name": "wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 535384, - "sha256": "2d8a773b4f94a9e2e6fc13923101ca6e66384330489014cb5e4c3968dc941a21" + "name": "wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/wiscore_rak3401_auto_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 533976, + "sha256": "46eb2e9b0b5cdd61c2384953df937934cf41b35d4915e14fb657725a5ed4aa5c" } } }, @@ -574,22 +574,22 @@ "storage": "adaptive", "files": { "uf2": { - "name": "update-wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", + "name": "update-wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", "size": 88064, - "sha256": "40bb22fcfb90aae335a8bb6bb6e876f3407d87407ead8ee3908bb0ba0b2541a7" + "sha256": "a267c699219a74de9fcb55093c7f7979e7fad515ddc07ec386705d7f80313883" }, "zip": { - "name": "wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "size": 192833, - "sha256": "d3c12363a620083f18e828e724e0af67a2b141b21dc4768d0707cfaadc32b98d" + "name": "wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "size": 192834, + "sha256": "0a7d556cbe68f9698c20ce3e892d1f39a6fc08f1cfb178df7128b340f9f8dc35" }, "hex": { - "name": "wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 535780, - "sha256": "963cb6e47179551ce975b54502fb8dbe62a9601ba7432cbf8b23cf4c86f2a6ef" + "name": "wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/wiscore_rak4631_auto_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 535644, + "sha256": "48bc983c66ff5f0fd764f3e5f64576a8ddfb3b260ba0d562f3c8f4072ba99c1d" } } }, @@ -603,22 +603,22 @@ "storage": "internal", "files": { "uf2": { - "name": "update-wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 84992, - "sha256": "0d45f1d73fdcea41bf647347096b33919ae69ce957a06d6ff0e48be8767f1081" + "name": "update-wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 83968, + "sha256": "eae9e3887877d0e45c60bf56ce06911592484dfdd2826b034651e1b42d118ac1" }, "zip": { - "name": "wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.zip", + "name": "wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.zip", "size": 192834, - "sha256": "716520cce6e46b7b2d84272a8a5a29d1c38344dfef7455d974969b700527fda3" + "sha256": "48e07eaac9926f0d9d569f484d35f07eca857dd76d2b688da2fe7e2bb69ed71d" }, "hex": { - "name": "wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.11_s140_6.1.1.hex", - "size": 530862, - "sha256": "d7503e65e4f37d028c58650bd31236ca493cff5dae1700e00baa8bbe2dbdca2a" + "name": "wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/wismesh_tag_bootloader-v0.11.0-OTAFIX2.4.12_s140_6.1.1.hex", + "size": 529542, + "sha256": "b42c3ba9c9f759bb3a88b5fa4bccb0752d9f54e19d4c473183a844a1ce5c96d5" } } }, @@ -642,22 +642,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 86016, - "sha256": "1c6f18a21e0a6992f0ec2cb8f48e79ada5ccd7111b5b53f73b679c4dad10a9f5" + "name": "update-xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 84992, + "sha256": "1755dd8ab6c38f571807131e1d2e27a087dbbab9341145a742b1db651eec9c08" }, "zip": { - "name": "xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "name": "xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", "size": 194546, - "sha256": "f0bb4832a96a625d18edb272fdc6ae2bc82ca291731f3edcd178c1c22bbe322d" + "sha256": "2f943dbac7dd22a7c63712eb138cc8f9078a20f6975079793124f10f1c246d87" }, "hex": { - "name": "xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "size": 537328, - "sha256": "dbef733404fe15e06e0d6ffa006f34094a53a19fc62314cc28764afe3deb5154" + "name": "xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "size": 536052, + "sha256": "43d6aa9322105559908551516c5a9f29ccf6f86f7c2391e0e8dfb23b4a32d280" } } }, @@ -671,22 +671,22 @@ "storage": "qspi", "files": { "uf2": { - "name": "update-xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/update-xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_mbr.uf2", - "size": 86016, - "sha256": "3a412b6eb12199b68cc3651082f0fb9f16ea1398c75537138d96068776e7084a" + "name": "update-xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/update-xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.12_mbr.uf2", + "size": 85504, + "sha256": "9c4f7135bcfdf9af1c40679bbee5fed4ee53dd9d569844bc48b7db1e3eb38c09" }, "zip": { - "name": "xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.zip", + "name": "xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.zip", "size": 194546, - "sha256": "954a5ee6b0d008e6918d91f0de5af7c80cf170c42ea9f8dc0957e9698a90f5c5" + "sha256": "3be6849bb5e66dc39ffc55c8c5206f2bada5c1b27e8b1e357337272eaa7ee29e" }, "hex": { - "name": "xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.11/xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.11_s140_7.3.0.hex", - "size": 537362, - "sha256": "07ccd6e07d9eebb6ce077302a393df2edb59c3106b93202a627f755c3980d53d" + "name": "xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "url": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12/xiao_nrf52840_ble_sense_bootloader-v0.11.0-OTAFIX2.4.12_s140_7.3.0.hex", + "size": 536086, + "sha256": "0ce9559cebbe1e0164169948f5bdd5e976316060cb72d8cb23a13de38de839b8" } } } @@ -698,7 +698,22 @@ "meshcoreHardware": [ "Heltec_tower_v2" ], - "reason": "Retired from normal releases in favor of the combined SD/internal profile. Existing internal-only bootloaders require a one-time local installation of the normal combined image via USB/BLE DFU or SWD; do not use a cross-profile LoRa update." + "reason": "Retired from normal releases in favor of the combined SD/internal profile. Existing internal-only bootloaders require a one-time local installation of the normal combined image via USB/BLE DFU or SWD; do not use a cross-profile LoRa update.", + "qualification": { + "status": "retired", + "sourceBoard": "heltec_mesh_tower_v2", + "storage": "internal", + "factoryIdentity": "verified", + "hardwareQualified": false, + "replacementProfile": "heltec_mesh_tower_v2_sdcard", + "localMigrationOnly": true, + "blockers": [ + "Installed internal-only identity requires local migration, not cross-profile LoRa self-update." + ], + "evidence": [ + "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.11" + ] + } }, { "id": "gat562_mesh_watch13", @@ -706,7 +721,20 @@ "meshcoreHardware": [ "GAT562_Mesh_Watch13" ], - "reason": "The watch routes a GAT562 bootloader LED output to its vibration motor and has onboard flash. It needs a watch-specific profile." + "reason": "A watch-specific QSPI source port avoids driving the vibration motor as an LED. It awaits physical power, DFU and update qualification; no verified download is available.", + "qualification": { + "status": "source-pending", + "sourceBoard": "gat562_mesh_watch13", + "storage": "qspi", + "factoryIdentity": "verified", + "hardwareQualified": false, + "blockers": [ + "Physical button, motor-off, flash and power-loss update qualification." + ], + "evidence": [ + "https://github.com/gat-iot/GAT562-family/blob/main/GAT562%20Mesh%20Watch13%20V1.0%20SCH.pdf" + ] + } }, { "id": "heltec_mesh_solar", @@ -714,7 +742,24 @@ "meshcoreHardware": [ "Heltec_mesh_solar" ], - "reason": "Mesh Solar has different LED and peripheral pins from the supported Heltec boards. It needs its own profile." + "reason": "A guarded internal-staging Solar source port avoids conflicting LED/QSPI/rail definitions, preserves the real factory identity and services the author-confirmed watchdog. Carrier-revision power/reset and long DFU/update watchdog behavior still require hardware qualification. No verified download is available.", + "qualification": { + "status": "source-pending", + "sourceBoard": "heltec_mesh_solar", + "storage": "internal", + "factoryIdentity": "verified", + "hardwareQualified": false, + "blockers": [ + "Carrier revision, factory entry, power/reset and long DFU/apply watchdog waveform qualification.", + "Matching internal-staging S140 6.1.1 exact-base delta and signed self-update failure/power-loss qualification.", + "External staging remains unsupported: actual flash/rail routing conflicts with BSP BMS/indicator definitions." + ], + "evidence": [ + "https://resource.heltec.cn/download/MeshSolar/Pin_map/MeshSolar_pinmap.jpg", + "https://github.com/meshtastic/firmware/pull/9337", + "https://github.com/HelTecAutomation/Heltec_nRF52/pull/8" + ] + } }, { "id": "lilygo_t_impulse_plus", @@ -722,7 +767,20 @@ "meshcoreHardware": [ "LilyGo_T_Impulse_Plus" ], - "reason": "T-Impulse Plus has different flash data pins and LED wiring from T-Echo Lite. It needs its own profile." + "reason": "A dedicated QSPI source port preserves T-Impulse Plus flash wiring, rail and controls. Physical flash, power, DFU and update qualification is still required; no verified download is available.", + "qualification": { + "status": "source-pending", + "sourceBoard": "lilygo_t_impulse_plus", + "storage": "qspi", + "factoryIdentity": "verified", + "hardwareQualified": false, + "blockers": [ + "Physical flash-rail, button, USB/BLE and power-loss update qualification." + ], + "evidence": [ + "https://github.com/Xinyuan-LilyGO/T-Impulse-Plus" + ] + } }, { "id": "lilygo_techo_card", @@ -730,7 +788,20 @@ "meshcoreHardware": [ "LilyGo_T-Echo_Card" ], - "reason": "T-Echo Card has its own NFC configuration, buttons and LED hardware. It needs a Card-specific profile." + "reason": "A Card-specific QSPI source port preserves its NFC configuration, flash rail and controls. Physical flash, USB/BLE and update qualification is still required; no verified download is available.", + "qualification": { + "status": "source-pending", + "sourceBoard": "lilygo_techo_card", + "storage": "qspi", + "factoryIdentity": "verified", + "hardwareQualified": false, + "blockers": [ + "Physical NFC, flash-rail, button, USB/BLE and power-loss update qualification." + ], + "evidence": [ + "https://github.com/Xinyuan-LilyGO/T-Echo-Card" + ] + } }, { "id": "meshtiny", @@ -738,7 +809,22 @@ "meshcoreHardware": [ "Meshtiny" ], - "reason": "Meshtiny has its own factory bootloader identity and controls. A dedicated Meshtiny profile has not been qualified." + "reason": "A dedicated Meshtiny QSPI source port preserves factory identity and real side/top controls. Physical flash, power, DFU and update qualification remains pending; no verified download is available.", + "qualification": { + "status": "source-pending", + "sourceBoard": "meshtiny", + "storage": "qspi", + "factoryIdentity": "verified", + "hardwareQualified": false, + "blockers": [ + "Physical side/top-button entry, flash and power-loss update qualification.", + "Matching application must use external raw staging; application flash macros cannot be inferred from the bootloader." + ], + "evidence": [ + "https://shop.mtoolstec.com/bootloader-update-on-meshtiny.html", + "https://dl.mtoolstec.com/meshtiny/bootloader.uf2" + ] + } }, { "id": "meshtracker_x1", @@ -746,7 +832,25 @@ "meshcoreHardware": [ "MeshTracker_X1" ], - "reason": "MeshTracker X1 has different flash wiring, power control and button polarity from Wio Tracker L1. It needs its own profile." + "reason": "A dedicated internal-staging S140-v7 qualification source port preserves factory USB identity and the active-high button without driving unverified flash or peripheral rails. Cold USB/reset/power behavior and matching internal-staging application updates still need hardware testing. No verified download is available.", + "qualification": { + "status": "source-pending", + "sourceBoard": "meshtracker_x1", + "storage": "internal", + "factoryIdentity": "verified", + "hardwareQualified": false, + "blockers": [ + "Cold USB, button-plus-USB, BLE, timeout and power-controller behavior during lengthy updates.", + "Matching internal-staging S140 7.3.0 application, exact-base delta and signed exact-profile self-update qualification.", + "External full-image staging remains unsupported until flash-rail/reset behavior is corroborated." + ], + "evidence": [ + "https://wiki.seeedstudio.com/sensecap_meshtracker_x1_meshcore/", + "https://files.seeedstudio.com/wiki/SenseCAP/Meshcore/MeshTrackerX1/Bootloader.zip", + "https://github.com/meshcore-dev/MeshCore/pull/3112", + "https://github.com/meshcore-dev/MeshCore/pull/3139" + ] + } }, { "id": "muzi_base", @@ -757,7 +861,20 @@ "muzi_base_uno", "muzi_base_uno_superIO" ], - "reason": "These four variants share Muzi Base hardware, which needs its own bootloader identity, LED polarity and flash configuration." + "reason": "A Muzi Base QSPI source port covers the four carrier variants with the real factory identity, LED polarity and flash pins. Physical DFU, flash and update qualification remains pending; no verified download is available.", + "qualification": { + "status": "source-pending", + "sourceBoard": "muzi_base", + "storage": "qspi", + "factoryIdentity": "verified", + "hardwareQualified": false, + "blockers": [ + "Physical carrier-variant, flash, USB/BLE and power-loss update qualification." + ], + "evidence": [ + "https://github.com/muzi-works/Adafruit_nRF52_Bootloader/tree/master/src/boards/muzi_base" + ] + } }, { "id": "nano_g2_ultra", @@ -765,7 +882,21 @@ "meshcoreHardware": [ "Nano_G2_Ultra" ], - "reason": "Nano G2 Ultra has its own flash and button wiring. It needs a dedicated profile." + "reason": "A dedicated Nano G2 Ultra QSPI source port preserves designer-confirmed flash/buttons and factory USB identity. Physical single-button DFU, flash and update qualification is pending; no verified download is available.", + "qualification": { + "status": "source-pending", + "sourceBoard": "nano_g2_ultra", + "storage": "qspi", + "factoryIdentity": "verified", + "hardwareQualified": false, + "blockers": [ + "Physical single-button USB/BLE entry, flash and power-loss update qualification." + ], + "evidence": [ + "https://wiki.bqvoy.com/meshtastic/nano_g2_ultra/nanog2ultra_19_jun_2023.pdf", + "https://wiki.bqvoy.com/meshtastic/nano_g2_ultra/nano_g2_nrf52840_bootloader-0.6.4_s140_6.1.1.hex.zip" + ] + } }, { "id": "thinknode_m4", @@ -773,7 +904,24 @@ "meshcoreHardware": [ "ThinkNode_M4" ], - "reason": "ThinkNode M4 has different LED, button and power-control pins from the supported ThinkNodes. It needs its own profile." + "reason": "M4 has a bespoke PCB and manufacturer-specified 2 MB external flash, but verified flash routing, factory bootloader identity and safe power/reset behavior are missing. An M3 profile or guessed internal-only definition is unsafe.", + "qualification": { + "status": "source-blocked", + "sourceBoard": null, + "storage": "unknown", + "factoryIdentity": "unverified", + "hardwareQualified": false, + "blockers": [ + "Factory USB descriptor, INFO_UF2 board identity and bootloader installation route.", + "Actual 2 MB external-flash pin routing.", + "Safe power/reset/rail behavior." + ], + "evidence": [ + "https://github.com/meshcore-dev/MeshCore/pull/3463", + "https://github.com/meshtastic/firmware/pull/8754", + "https://static-cdn.elecrow.com/wiki/ThinkNode-M4_Power_Bank_LoRa_Device_with_Meshtastic_Function_Powered_By_nRF52840.html" + ] + } }, { "id": "thinknode_m8", @@ -781,7 +929,23 @@ "meshcoreHardware": [ "ThinkNode_M8" ], - "reason": "A dedicated M8 bootloader is awaiting factory USB identification and hardware testing. No verified download is available." + "reason": "A V0.3 QSPI compile-only source port exists, but its factory USB identity and disputed power pin are unresolved. Its zero USB identity is an invalid compiler fixture, not a flashable image. No verified download is available.", + "qualification": { + "status": "source-pending", + "sourceBoard": "thinknode_m8", + "storage": "qspi", + "factoryIdentity": "unverified", + "hardwareQualified": false, + "blockers": [ + "Factory USB descriptor and INFO_UF2 board identity; replace invalid zero identity before any hardware use.", + "Resolve P0.13 power-enable versus I2C rail and production hardware revision.", + "Physical power/entry, flash and interrupted update qualification." + ], + "evidence": [ + "https://github.com/meshcore-dev/MeshCore/pull/3180", + "https://github.com/meshtastic/firmware/pull/11226" + ] + } }, { "id": "wio_wm1110", @@ -789,14 +953,35 @@ "meshcoreHardware": [ "wio_wm1110" ], - "reason": "This firmware targets the Wio WM1110 Dev Kit (SDK carrier), whose LEDs and controls differ from Wio Tracker 1110 and XIAO. Its bootloader identity and installation route still need qualification." + "reason": "This target is the Wio WM1110 Dev Kit (SDK carrier), not Tracker 1110 or XIAO. Official installation is J-Link/SWD and its connector is CH340C USB-UART, not proven native-USB UF2. The official blank application starts at address zero, not an identified Adafruit factory bootloader layout. Exact bootloader/SoftDevice identity, UART recovery and NFC configuration still need verification.", + "qualification": { + "status": "source-blocked", + "sourceBoard": null, + "storage": "unknown", + "factoryIdentity": "unverified", + "hardwareQualified": false, + "blockers": [ + "Exact bootloader/SoftDevice identity and layout; confirmed J-Link/SWD installation alone does not establish factory UF2 identity.", + "Safe Dev Kit UART transport/entry, recovery protocol and NFC configuration (CH340C UART0 TX P0.08/RX P0.06).", + "Native USB reachability is unproven; do not treat CH340C USB-UART as UF2." + ], + "evidence": [ + "https://wiki.seeedstudio.com/Get_Started_with_Wio-WM1110_Dev_Kit/", + "https://github.com/Seeed-Studio/Seeed_Wio_WM1110_Dev_Board/blob/e34742993932c97731f513ffa3028d284b163d31/pca10056/blank/ses_blinky/wm1110_dev_kit_pca10056.emProject", + "https://wiki.seeedstudio.com/Wio-WM1110_Dev_Kit/Introduction/", + "https://github.com/meshcore-dev/MeshCore/pull/982" + ] + } } ], - "profileSourceOverrides": { - "heltec_mesh_tower_v2_sdcard": { - "source_tag": "0.11.0-OTAFIX2.4.11", - "source_commit": "39177b9bbaa894352141734350d6ef2fab18dc4f", - "workflow_run": 37072081200 - } + "coverage": { + "releasedProfiles": 23, + "releasedHardware": 40, + "pendingProfiles": 9, + "pendingHardware": 12, + "blockedProfiles": 2, + "blockedHardware": 2, + "retiredProfiles": 1, + "retiredHardware": 1 } } diff --git a/docs/nrf52_embedded_bootloader.md b/docs/nrf52_embedded_bootloader.md new file mode 100644 index 000000000..a522289b1 --- /dev/null +++ b/docs/nrf52_embedded_bootloader.md @@ -0,0 +1,102 @@ + + + + +# Embedded nRF52 bootloader update + +An opt-in Repeater or Room Server build can carry its application firmware and +the exact board's [OTAFIX 2.4.12 bootloader](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.12) +in one application image. Install that application first, then activate the +bootloader replacement with an authenticated admin command. Booting the image +does not flash the bootloader automatically. + +This follows the internal-flash bridge in +[MeshEnvy firmware PR #2](https://github.com/MeshEnvy/meshcore-firmware/pull/2). +The replacement stays inside the running application's verified image until +Nordic's MBR copies it to the bootloader region. It needs no external staging +storage and does not replace the SoftDevice or change the filesystem layout. + +## Build + +From the repository root, replace `TARGET` with the exact OTA-enabled nRF52840 +Repeater or Room Server target: + +```sh +bash build.sh build-firmware TARGET --with-bootloader 2.4.12 \ + --firmware-version v1.18.0.1-halo-keymind-cascade-dev +``` + +For direct PlatformIO builds, the equivalent opt-in is: + +```sh +MESHCORE_NRF52_EMBED_BOOTLOADER=2.4.12 pio run -e TARGET +``` + +The generator matches the board and storage to the 2.4.12 catalog and verifies +the release asset before embedding only its 40 KiB bootloader region. Host +validation covers 22 profiles. SenseCAP Solar P1 is excluded because its shared +XIAO board ID and different device name need separate identity support. +Unsupported profiles are refused. The usual application, +capacity and memory checks still apply: the resource adds 40 KiB plus update +code, so some profiles will not fit. The build does not remove features or +shrink the filesystem to make room. + +Opt-in outputs include `-with-bootloader-2.4.12` in their names. Ordinary builds +and their output names stay unchanged. The embedded image remains an +application UF2/DFU package, not a combined SoftDevice/bootloader DFU package. + +An installed bootloader with valid compatible identity metadata must match the +candidate's board and runtime layout. For the explicitly qualified legacy +RAK4631 transition, also supply the exact installed bootloader image: + +```sh +MESHCORE_NRF52_LEGACY_BOOTLOADER=/absolute/path/full.hex \ + bash build.sh build-firmware RAK_4631_repeater --with-bootloader 2.4.12 +``` + +The legacy exception binds the entire padded bootloader region by SHA-256. +It accepts the qualified MeshEnvy `0.9.2-ev1` RAK4631 image, whose full-region +SHA-256 is `64bfe69d56eefc2d0fe51770902aff70ea0cc06cad261d3d219e4a3c0efac8dc`. +A matching version string alone is insufficient; another board's image or an +unknown stock bootloader is not accepted by that exception. + +## Install and activate + +1. Install the application through its normal USB UF2 or serial/Bluetooth DFU + route. LoRa delivery requires an already working, compatible application OTA + updater. An internal-only legacy receiver needs a delta from its exact + installed application; this image does not give a stock bootloader its first + LoRa update capability. +2. After the new application is running, use the local CLI or authenticated + remote admin CLI: + + ```text + ota bootloader embedded status + ota bootloader embedded audit + ota bootloader embedded install HASH16 + ``` + + Replace `HASH16` with the image confirmation shown by `status`. Save the audit + before installation. The acknowledgment means armed; replacement waits for + the reply and queued/in-flight radio transmissions to drain. +3. After reboot, check `get bootloader.ver` and repeat the audit. Confirm 2.4.12 + and compare the application, filesystem, settings and SoftDevice hashes. + Check status before repeating a command whose reply was lost. + +The bridge validates the live application, candidate hash, installed identity, +chip/layout, MBR addresses, watchdog and flash protection before copying. A +protected region is refused; the bridge does not unlock it. Existing staged +`ota bootloader status` and `ota bootloader install MID8 HASH16` commands keep +their separate signed-package behavior. See +[bootloader updates over LoRa](ota_nrf52_bootloader_update.md) for that path. + +## Qualification + +The source PR documents one successful RAK4631 migration with unchanged +application, filesystem, settings and SoftDevice hashes. Its later guards have +host checks, but the recorded hardware run predates the final PR build. +Other boards and power-cut recovery are not hardware-qualified by that test. +Keep local recovery access when qualifying a new transition; an older loader's +initial application-delta stage may itself be destructive. The MBR parameter +page is intentionally used during the bootloader copy and is outside the +unchanged-settings claim. diff --git a/docs/ota_nrf52_bootloader_update.md b/docs/ota_nrf52_bootloader_update.md index cf7de35bf..6aee11e59 100644 --- a/docs/ota_nrf52_bootloader_update.md +++ b/docs/ota_nrf52_bootloader_update.md @@ -4,6 +4,10 @@ # nRF52 bootloader updates over LoRa +For an application image that carries its own board-matched 2.4.12 replacement, +see [embedded nRF52 bootloader updates](nrf52_embedded_bootloader.md). That opt-in +bridge uses a separate admin activation command after the application is running. + For **1.17.1.5**, use the exact board/storage profile from [OTAFIX 2.4.6](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/0.11.0-OTAFIX2.4.6) for nRF52 OTAFIX installations. New internal-flash hybrid receivers require diff --git a/platformio.ini b/platformio.ini index 59c05ccad..d9e160982 100644 --- a/platformio.ini +++ b/platformio.ini @@ -185,6 +185,7 @@ extra_scripts = create-uf2.py pre:scripts/nrf52_hil_irq_observer.py pre:scripts/nrf52_hil_spim1.py pre:scripts/nrf52_internal_bootloader_link.py + pre:scripts/nrf52_embedded_bootloader_pio.py ; Nordic's builder otherwise overwrites the LTO-aware archive tools. pre:scripts/nrf52_lto_toolchain.py ; Role-scoped trims: Companion keeps bounded crypto stack, fonts and features. diff --git a/scripts/firmware_build_recipe.py b/scripts/firmware_build_recipe.py index 1255c657d..086fc41ce 100644 --- a/scripts/firmware_build_recipe.py +++ b/scripts/firmware_build_recipe.py @@ -21,6 +21,7 @@ RECIPE_ENVIRONMENT = ( "MESHCORE_COMPANION_RADIO_FULL", "MESHCORE_ESP32_FULL_PARTITION_TABLE", "MESHCORE_NRF52_INTERNAL_BOOTLOADER_UPDATE", "MESHCORE_FORCE_LORA_OTA", "MESHCORE_NRF52_LEGACY_FLASH_OTA", "MESHCORE_REDUCED_TLS", + "MESHCORE_NRF52_EMBED_BOOTLOADER", "MESHCORE_NRF52_LEGACY_BOOTLOADER", ) PACKAGE_SUFFIXES = (".bin", "-merged.bin", ".uf2", ".zip", ".hex", ".capabilities.json", ".memory.json", ".font-license.txt", @@ -184,6 +185,10 @@ def recipe_digest(args, options): if name.startswith(("PLATFORMIO_", "MESHCORE_"))} names.discard("MESHCORE_RECIPE_ORIGINAL_FLAGS") environment = {name: os.environ.get(name) for name in names} + # A legacy bridge binds the supplied bytes, not just their external path. + legacy = os.environ.get("MESHCORE_NRF52_LEGACY_BOOTLOADER") + if legacy: + environment["MESHCORE_NRF52_LEGACY_BOOTLOADER_SHA256"] = file_digest(Path(legacy)) return canonical_digest({ "schema_version": SCHEMA, "source": source_digest(args.root, args.output, expected_commit=args.source_commit), diff --git a/scripts/nrf52_embedded_bootloader.py b/scripts/nrf52_embedded_bootloader.py new file mode 100644 index 000000000..f193b6f12 --- /dev/null +++ b/scripts/nrf52_embedded_bootloader.py @@ -0,0 +1,285 @@ +"""Embed an exact published OTAFIX image in an opt-in infrastructure build.""" +from __future__ import annotations + +import binascii +import hashlib +import io +import json +from pathlib import Path +import re +import stat +import struct +import sys +import tempfile +from urllib.request import urlopen +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +VERSION = "2.4.12" +BOOT_VERSION = 0x02040CFF +MANIFEST_SHA256 = "d2f62a4ec9b7c5643d8ed787d9e1710a2fabac34607a0bbb6ea5883a458e6df3" +LEGACY_SHA256 = "64bfe69d56eefc2d0fe51770902aff70ea0cc06cad261d3d219e4a3c0efac8dc" +BOOT_BASE, BOOT_SIZE = 0xF4000, 0xA000 +REPOSITORY = "mikecarper/Adafruit_nRF52_Bootloader_OTAFIX" +TAG = "v0.11.0-OTAFIX" + VERSION +MAX_ZIP_SIZE = 2 * 1024 * 1024 + + +def _unique_object(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("duplicate JSON key: " + key) + result[key] = value + return result + + +def _json(data): + return json.loads(data.decode("utf-8"), object_pairs_hook=_unique_object) + + +def load_manifest(project=ROOT): + data = (Path(project) / "docs/_data/bootloader_manifest.json").read_bytes() + if hashlib.sha256(data).hexdigest() != MANIFEST_SHA256: + raise ValueError("embedded bootloader requires the pinned OTAFIX 2.4.12 download manifest") + manifest = _json(data) + if (manifest.get("schemaVersion") != 1 or manifest.get("repository") != REPOSITORY + or manifest.get("tag") != TAG or manifest.get("version") != VERSION + or manifest.get("prerelease") is not False): + raise ValueError("unexpected embedded bootloader release identity") + return manifest + + +def select_profile(manifest, target): + from generate_mesh_america_release import target_profiles + parsed = target_profiles([target])[target] + if parsed["role"] not in ("repeater", "room"): + raise ValueError("embedded bootloader is limited to repeater and room-server roles") + names = {parsed["sourceHardware"], parsed["hardware"]} + if any(parsed["sourceHardware"] in item["meshcoreHardware"] + for item in manifest.get("unavailableProfiles", ())): + raise ValueError("this exact hardware has no qualified OTAFIX download") + matches = [item for item in manifest["profiles"] + if names.intersection(item["meshcoreHardware"])] + if len(matches) != 1: + raise ValueError("hardware must map to one exact published bootloader profile") + return matches[0], parsed + + +def _asset(profile): + asset = profile["files"]["zip"] + name, size, digest = asset["name"], asset["size"], asset["sha256"] + if (not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*\.zip", name) + or type(size) is not int or not 0 < size <= MAX_ZIP_SIZE + or not re.fullmatch(r"[0-9a-f]{64}", digest) + or asset["url"] != f"https://github.com/{REPOSITORY}/releases/download/{TAG}/{name}"): + raise ValueError("invalid pinned bootloader ZIP metadata") + return asset + + +def download_zip(profile, cache_dir): + asset = _asset(profile) + cache = Path(cache_dir) + cache.mkdir(parents=True, exist_ok=True) + path = cache / (asset["sha256"] + "-" + asset["name"]) + + def valid(data): + return len(data) == asset["size"] and hashlib.sha256(data).hexdigest() == asset["sha256"] + + if path.is_file() and path.stat().st_size == asset["size"]: + data = path.read_bytes() + if valid(data): + return data + with urlopen(asset["url"], timeout=60) as response: + data = response.read(asset["size"] + 1) + if not valid(data): + raise ValueError("downloaded bootloader ZIP size/SHA-256 mismatch") + temporary = None + try: + with tempfile.NamedTemporaryFile(dir=cache, prefix="otafix-", delete=False) as stream: + temporary = Path(stream.name) + stream.write(data) + temporary.replace(path) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + return data + + +def _unsigned_init(packet, metadata, firmware): + if len(packet) < 12 or len(packet) > 4096: + raise ValueError("invalid Legacy DFU init-packet length") + device, revision, app_version, count = struct.unpack_from(" 64 or len(packet) != 12 + count * 2: + raise ValueError("invalid Legacy DFU SoftDevice requirements") + requirements = list(struct.unpack_from(f"<{count}H", packet, 10)) + crc = struct.unpack_from("> 16) not in (0, stat.S_IFREG) + or not 0 < entry.file_size <= (512 * 1024 if entry.filename.endswith(".bin") else 65536)): + raise ValueError("unsupported bootloader ZIP member") + document = _json(archive.read("manifest.json")) + if set(document) != {"manifest"}: + raise ValueError("invalid bootloader ZIP manifest root") + manifest = document["manifest"] + if set(manifest) != {"dfu_version", "softdevice_bootloader"} or manifest["dfu_version"] != 0.5: + raise ValueError("expected an unsigned Legacy SoftDevice/bootloader package") + entry = manifest["softdevice_bootloader"] + if set(entry) != {"bin_file", "dat_file", "sd_size", "bl_size", "init_packet_data"}: + raise ValueError("invalid combined bootloader manifest entry") + if set(names) != {"manifest.json", entry["bin_file"], entry["dat_file"]}: + raise ValueError("bootloader ZIP references do not match its members") + if not entry["bin_file"].endswith(".bin") or not entry["dat_file"].endswith(".dat"): + raise ValueError("bootloader ZIP has incorrect member extensions") + firmware = archive.read(entry["bin_file"]) + packet = archive.read(entry["dat_file"]) + sd_size, bl_size = entry["sd_size"], entry["bl_size"] + if (type(sd_size) is not int or type(bl_size) is not int or sd_size <= 0 + or sd_size % 4 or bl_size != BOOT_SIZE or sd_size + bl_size != len(firmware)): + raise ValueError("invalid SoftDevice/bootloader payload geometry") + _unsigned_init(packet, entry["init_packet_data"], firmware) + image = firmware[sd_size:] + sys.path.insert(0, str(Path(project) / "tools/mota")) + import motalib as ml + identity = ml.parse_bootloader_identity(image) + sp, reset = struct.unpack_from(""]) + print(f"Embedded OTAFIX {metadata['release']}: {metadata['profile']}; 40960 bytes; " + f"SHA256 {metadata['candidate_sha256']}") diff --git a/scripts/nrf52_internal_bootloader_link.py b/scripts/nrf52_internal_bootloader_link.py index 2d5381ec2..4ef6e97a9 100644 --- a/scripts/nrf52_internal_bootloader_link.py +++ b/scripts/nrf52_internal_bootloader_link.py @@ -33,6 +33,13 @@ requested = ( # replace its own bootloader. Keep its ordinary application OTA path and # linker when explicitly building a legacy-compatible destination image. legacy_flash = os.environ.get("MESHCORE_NRF52_LEGACY_FLASH_OTA") +if os.environ.get("MESHCORE_NRF52_LEGACY_BOOTLOADER"): + if os.environ.get("MESHCORE_NRF52_EMBED_BOOTLOADER") != "2.4.12": + raise RuntimeError("legacy bootloader input requires the embedded 2.4.12 variant") + if legacy_flash not in (None, "1"): + raise RuntimeError("legacy bootloader delivery cannot use retained-RAM staging") + legacy_flash = "1" + requested = True # the resource hook separately verifies the exact RAK legacy profile if legacy_flash not in (None, "0", "1"): raise RuntimeError("MESHCORE_NRF52_LEGACY_FLASH_OTA must be 0 or 1") if legacy_flash == "1": diff --git a/scripts/qualify_nrf52_embedded_bootloader.py b/scripts/qualify_nrf52_embedded_bootloader.py new file mode 100644 index 000000000..8cf2e4250 --- /dev/null +++ b/scripts/qualify_nrf52_embedded_bootloader.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Bind an embedded bootloader claim to the actual packaged application.""" +import argparse +import hashlib +import json +from pathlib import Path +import sys +import zipfile + +from nrf52_embedded_bootloader import (ROOT, BOOT_SIZE, VERSION, MANIFEST_SHA256, + LEGACY_SHA256, load_manifest, select_profile, download_zip, validate_candidate_zip) + +sys.path.insert(0, str(ROOT / "tools/mota")) +from motalib import parse_endf, parse_endf_ident, parse_nrf52_layout, target_id_for_env + + +def qualify(directory, manifest_path): + directory, manifest_path = Path(directory), Path(manifest_path) + manifest = json.loads(manifest_path.read_text()) + if manifest.get("platform") != "NRF52_PLATFORM" or manifest.get("verified") is not True: + raise ValueError("embedded bootloader requires qualified nRF52 firmware") + profile, _ = select_profile(load_manifest(), manifest["target"]) + candidate, identity = validate_candidate_zip(download_zip(profile, ROOT / ".pio/otafix-cache"), profile) + resource = directory / "bootloader-migration-resource" + metadata = json.loads((resource / "BootloaderMigrationResource.json").read_text()) + if (metadata.get("profile") != profile["id"] or metadata.get("release") != VERSION + or metadata.get("manifest_sha256") != MANIFEST_SHA256 + or metadata.get("asset_sha256") != profile["files"]["zip"]["sha256"] + or metadata.get("candidate_sha256") != hashlib.sha256(candidate).hexdigest() + or metadata.get("legacy_sha256") not in (None, LEGACY_SHA256) + or (resource / "BootloaderMigrationResource.bin").read_bytes() != candidate): + raise ValueError("embedded bootloader build resource differs from its pinned release") + with zipfile.ZipFile(directory / "firmware.zip") as package: + entry = json.loads(package.read("manifest.json"))["manifest"]["application"] + application = package.read(entry["bin_file"]) + body, _ = parse_endf(application) + app_identity = parse_endf_ident(application) + layout = parse_nrf52_layout(application) + offset = body.find(candidate) + if (not app_identity or app_identity.target_id != target_id_for_env(manifest["target"]) + or not layout or layout.app_base != identity.app_base + or len(application) > layout.linked_app_end - layout.app_base + or offset < 0 or offset % 4 or body.find(candidate, offset + 1) >= 0 + or offset + BOOT_SIZE > len(body) + or b"BL embedded 2.4.12" not in body): + raise ValueError("packaged application lacks the complete aligned embedded bootloader or activation code") + manifest["embedded_bootloader"] = { + "release": VERSION, "profile": profile["id"], + "candidate_sha256": hashlib.sha256(candidate).hexdigest(), + "asset_sha256": profile["files"]["zip"]["sha256"], + "manifest_sha256": MANIFEST_SHA256, + "legacy_sha256": metadata.get("legacy_sha256"), + "app_offset": offset, "image_bytes": BOOT_SIZE, + "activation": "ota bootloader embedded install HASH16", + } + manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + return manifest["embedded_bootloader"] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("directory", type=Path) + parser.add_argument("manifest", type=Path) + args = parser.parse_args() + try: + result = qualify(args.directory, args.manifest) + except (OSError, ValueError, KeyError, zipfile.BadZipFile) as exc: + parser.exit(1, f"Embedded bootloader qualification failed: {exc}\n") + print(f"Verified embedded OTAFIX {result['release']}: {result['profile']}; {result['image_bytes']} bytes") diff --git a/src/Mesh.cpp b/src/Mesh.cpp index b64a5fafc..284c462e3 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -10,6 +10,9 @@ #include "helpers/ota/OtaContext.h" // OTA mesh-integration is centralized here so every role gets it #include "helpers/ota/OtaSpeedConfig.h" #include "helpers/ota/OtaProtocol.h" // decode_adv -> the `ota neighbors` discovery table +#if defined(OTA_BOOTLOADER_MIGRATION) +#include "helpers/ota/BootloaderMigration.h" +#endif #if MESH_ENABLE_OTA_KISS_DIRECT #include "helpers/ota/OtaDirectWire.h" #endif @@ -419,6 +422,9 @@ void Mesh::loop() { } bool Mesh::hasPendingOtaApply() const { +#if defined(OTA_BOOTLOADER_MIGRATION) + if (ota::bootloader_migration_pending()) return true; +#endif #if defined(ENABLE_OTA) && !defined(OTA_SEEDER_ONLY) // A released dynamic context cannot hold a pending apply: the context is only // handed back once apply_pending is clear. @@ -474,6 +480,17 @@ void __attribute__((noinline)) Mesh::serviceLoopMaintenance() { } } #if defined(ENABLE_OTA) +#if defined(OTA_BOOTLOADER_MIGRATION) + if (ota::bootloader_migration_pending()) { + // This bridge owns no OTA workspace. Service it even after a dynamic + // context has been released, and never allocate one just to poll it. + const ota::OtaContext* context = ota::ota_context_if_active(); + const bool ota_idle = !context || context->embeddedBootloaderIdle(); + const bool outbound_idle = !hasPendingRadioWork(); + const bool ready = outbound_idle && ota_idle && prepareForOtaReboot(); + ota::bootloader_migration_tick(ready, ota_idle); + } +#endif #if OTA_DYNAMIC_CONTEXT // Nothing below can run without storage, and a released context holds no // pending apply or egress. Bail before any ota_ctx() dereference. diff --git a/src/helpers/ota/BootloaderMigration.cpp b/src/helpers/ota/BootloaderMigration.cpp new file mode 100644 index 000000000..96ec2f475 --- /dev/null +++ b/src/helpers/ota/BootloaderMigration.cpp @@ -0,0 +1,260 @@ +#if defined(OTA_BOOTLOADER_MIGRATION) +#if !defined(NRF52_PLATFORM) || !defined(ENABLE_OTA) +#error Bootloader migration requires nRF52 OTA infrastructure firmware +#endif + +#include "BootloaderMigration.h" + +#include "BootloaderMigrationPolicy.h" +#include "BootloaderMigrationResource.h" +#include "OtaSelf.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mesh { +namespace ota { +static_assert(migration::ACL_READ_DENIED == ACL_ACL_PERM_READ_Msk, "ACL read permission bit"); +static_assert(migration::ACL_WRITE_DENIED == ACL_ACL_PERM_WRITE_Msk, "ACL write permission bit"); +namespace { +bool pending = false; +uint32_t armed_at = 0; +uint8_t last_result = 0; +bool result_is_current = false; +bool already_installed = false; + +// The pinned framework's historical SVC wrapper has no memory clobber or IPA +// barrier. The MBR reads this pointed-to command after exception entry, which +// must remain visible to the compiler even under whole-program optimization. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wreturn-type" +__attribute__((naked, noipa)) uint32_t migration_mbr_command(sd_mbr_command_t *) { + __asm volatile("svc %0\n" + "bx lr" + : + : "I"(SD_MBR_COMMAND) + : "r0", "memory"); +} +#pragma GCC diagnostic pop + +void hex8(const uint8_t *value, char output[17]) { + static const char digits[] = "0123456789ABCDEF"; + for (unsigned i = 0; i < 8; ++i) { + output[2 * i] = digits[value[i] >> 4]; + output[2 * i + 1] = digits[value[i] & 15]; + } + output[16] = 0; +} + +void hash_region(uint32_t address, uint32_t size, uint8_t output[32]) { + SHA256 hash; + hash.reset(); + uint8_t buffer[128]; + const volatile uint8_t *source = reinterpret_cast(address); + while (size) { + const uint32_t take = size < sizeof(buffer) ? size : sizeof(buffer); + for (uint32_t i = 0; i < take; ++i) + buffer[i] = source[i]; + hash.update(buffer, take); + source += take; + size -= take; + } + hash.finalize(output, 32); +} + +uint8_t retained_result() { + if (result_is_current) return last_result; + uint8_t enabled = 0; + uint32_t result = 0; + if (sd_softdevice_is_enabled(&enabled) != NRF_SUCCESS) return 0; + if (enabled) { + if (sd_power_gpregret_get(0, &result) != NRF_SUCCESS) return 0; + } else { + result = NRF_POWER->GPREGRET; + } + return result >= 0xC0 && result <= 0xCF ? result & 15 : last_result; +} + +// Called only after the operator reply has drained and SoftDevice is stopped. +// GPREGRET (not GPREGRET2) survives this legacy loader's diagnostic writes. +void fail_and_reset(uint8_t result) { + NRF_POWER->GPREGRET = 0xC0u | (result & 15u); + NVIC_SystemReset(); + while (true) { + } +} + +void install() { + SelfFwInfo app; + if (!ota_self_firmware(app) || !app.valid) { + pending = false; + last_result = 1; + return; + } + uint8_t enabled = 0; + if (sd_softdevice_is_enabled(&enabled) != NRF_SUCCESS || + (enabled && sd_softdevice_disable() != NRF_SUCCESS)) { + pending = false; + last_result = 2; + return; + } + + // ACL is SoftDevice-owned while enabled. Inspect it only after disabling + // SoftDevice; no ACL/MPU writes or attempts to defeat locked regions. + migration::Snapshot snapshot = {}; + snapshot.part = NRF_FICR->INFO.PART; + snapshot.flash_page_size = NRF_FICR->CODEPAGESIZE; + snapshot.flash_pages = NRF_FICR->CODESIZE; + for (unsigned i = 0; i < 8; ++i) { + snapshot.acl[i] = { NRF_ACL->ACL[i].ADDR, NRF_ACL->ACL[i].SIZE, NRF_ACL->ACL[i].PERM }; + const auto ®ion = snapshot.acl[i]; + if ((region.permissions & migration::ACL_READ_DENIED) && + (migration::overlaps(region.address, region.size, 0, migration::APP_CEILING) || + migration::overlaps(region.address, region.size, migration::BOOT_BASE, migration::SETTINGS_BASE))) + fail_and_reset(9); + } + snapshot.app_base = mota_nrf52_app_base(); + snapshot.app_ceiling = static_cast(reinterpret_cast(__flash_arduino_end)); + snapshot.expected_softdevice_fwid = migration_softdevice_fwid; + snapshot.softdevice_size = *reinterpret_cast(0x3008); + snapshot.softdevice_fwid = *reinterpret_cast(0x300C); + snapshot.mbr_boot_override = *reinterpret_cast(0xFF8); + snapshot.mbr_params_override = *reinterpret_cast(0xFFC); + snapshot.uicr_boot = NRF_UICR->NRFFW[0]; + snapshot.uicr_params = NRF_UICR->NRFFW[1]; + snapshot.source = static_cast(reinterpret_cast(migration_candidate)); + snapshot.source_size = sizeof(migration_candidate); + snapshot.app_body_size = app.body_len; + snapshot.app_image_size = app.image_len; + const auto gate = migration::preflight(snapshot); + if (gate != migration::Gate::Ok) fail_and_reset(3 + static_cast(gate)); + if (snapshot.app_base != migration_app_base) fail_and_reset(5); + + uint8_t digest[32]; + hash_region(snapshot.source, snapshot.source_size, digest); + if (memcmp(digest, migration_candidate_sha256, sizeof(digest)) != 0) fail_and_reset(10); + OtaBootloaderIdentity candidate; + if (!ota_bootloader_vector_sane(migration_candidate) || + !ota_bootloader_candidate_identity_from_image(migration_candidate, migration::BOOT_SIZE, candidate) || + !migration::candidate_matches_resource(candidate, migration_board_id, migration_device_name, + migration_boot_version, snapshot.app_base, + snapshot.softdevice_fwid)) + fail_and_reset(10); + hash_region(migration::BOOT_BASE, migration::BOOT_SIZE, digest); + OtaBootloaderIdentity installed; + const uint8_t *boot = reinterpret_cast(migration::BOOT_BASE); + const bool valid_identity = ota_bootloader_identity_from_image(boot, migration::BOOT_SIZE, installed); + // A damaged or ambiguous modern manifest cannot masquerade as a legacy + // loader. Only the explicitly pinned, byte-exact legacy image is eligible. + if (!valid_identity) installed = OtaBootloaderIdentity(); + if (!migration::installed_can_migrate(installed, candidate, migration_has_legacy, + memcmp(digest, migration_legacy_sha256, sizeof(digest)) == 0, + snapshot.app_base, snapshot.softdevice_fwid)) + fail_and_reset(12); + + // Do not start an uninterruptible MBR copy under an unexpectedly short WDT. + if (NRF_WDT->RUNSTATUS && NRF_WDT->CRV < 5u * 32768u) fail_and_reset(13); + if (NRF_WDT->RUNSTATUS) { + for (unsigned i = 0; i < 8; ++i) { + if (NRF_WDT->RREN & (1u << i)) NRF_WDT->RR[i] = 0x6E524635u; + } + } + NRF_POWER->GPREGRET = 0; + NRF_POWER->GPREGRET2 = 0; + sd_mbr_command_t command = {}; + command.command = SD_MBR_COMMAND_COPY_BL; + command.params.copy_bl.bl_src = reinterpret_cast(snapshot.source); + command.params.copy_bl.bl_len = migration::BOOT_SIZE / sizeof(uint32_t); + // MBR owns the erase/copy and persistent recovery parameters. It never + // returns on success. No application, filesystem or settings erase occurs. + (void)migration_mbr_command(&command); + fail_and_reset(14); +} +} // namespace + +bool bootloader_migration_pending() { + return pending; +} + +void bootloader_migration_status(char *reply, size_t capacity) { + char hash[17]; + hex8(migration_candidate_sha256, hash); + snprintf(reply, capacity, "BL embedded 2.4.12: 40960 B; hash=%s; %s; last=%u", hash, + pending ? "armed" : already_installed ? "already installed" : "idle", retained_result()); +} + +bool bootloader_migration_request(const char *confirmation, char *reply, size_t capacity) { + char hash[17]; + hex8(migration_candidate_sha256, hash); + if (pending) { + snprintf(reply, capacity, "ERR bootloader migration is already armed"); + return false; + } + if (strcmp(confirmation, hash) != 0) { + snprintf(reply, capacity, "ERR confirm exact image: ota bootloader embedded install %s", hash); + return false; + } + // Bootloader diagnostics already read this memory-mapped region while + // SoftDevice runs. Detect a no-op before stopping it so BLE stays usable. + uint8_t digest[32]; + hash_region(migration::BOOT_BASE, migration::BOOT_SIZE, digest); + if (memcmp(digest, migration_candidate_sha256, sizeof(digest)) == 0) { + already_installed = true; + last_result = 0; + result_is_current = true; + snprintf(reply, capacity, "OK bootloader already installed"); + return true; + } + already_installed = false; + pending = true; + armed_at = millis(); + last_result = 0; + result_is_current = true; + snprintf(reply, capacity, "OK migration armed; checking exact old loader and protection after TX drains"); + return true; +} + +void bootloader_migration_audit(char *reply, size_t capacity) { + SelfFwInfo app; + if (!ota_self_firmware(app) || !app.valid) { + snprintf(reply, capacity, "ERR no valid application EndF"); + return; + } + uint8_t digest[32]; + char app_hash[17], settings_hash[17], fs_hash[17], sd_hash[17]; + hash_region(mota_nrf52_app_base(), app.image_len, digest); + hex8(digest, app_hash); + hash_region(migration::SETTINGS_BASE, 4096, digest); + hex8(digest, settings_hash); + const uint32_t fs_start = mota_nrf52_layout_stage_ceiling(); + hash_region(fs_start, migration::BOOT_BASE - fs_start, digest); + hex8(digest, fs_hash); + hash_region(0, mota_nrf52_app_base(), digest); + hex8(digest, sd_hash); + snprintf(reply, capacity, "app=%s settings=%s fs=%s sd=%s", app_hash, settings_hash, fs_hash, sd_hash); +} + +void bootloader_migration_tick(bool outbound_idle, bool ota_idle) { + if (!pending) return; + const uint32_t elapsed = millis() - armed_at; + // An incoming advert may have started autofetch since the admin command. + // Never disable SoftDevice while another OTA session owns the flash store. + if (!ota_idle) { + pending = false; + last_result = 16; + } else if (elapsed >= 30000u) { + pending = false; + last_result = 15; + } else if (elapsed >= 1500u && outbound_idle) { + install(); + } +} +} // namespace ota +} // namespace mesh +#endif diff --git a/src/helpers/ota/BootloaderMigration.h b/src/helpers/ota/BootloaderMigration.h new file mode 100644 index 000000000..d02384398 --- /dev/null +++ b/src/helpers/ota/BootloaderMigration.h @@ -0,0 +1,15 @@ +#pragma once + +#if defined(OTA_BOOTLOADER_MIGRATION) +#include + +namespace mesh { +namespace ota { +bool bootloader_migration_pending(); +bool bootloader_migration_request(const char *confirmation, char *reply, size_t capacity); +void bootloader_migration_status(char *reply, size_t capacity); +void bootloader_migration_audit(char *reply, size_t capacity); +void bootloader_migration_tick(bool outbound_idle, bool ota_idle); +} // namespace ota +} // namespace mesh +#endif diff --git a/src/helpers/ota/BootloaderMigrationPolicy.h b/src/helpers/ota/BootloaderMigrationPolicy.h new file mode 100644 index 000000000..646b116f4 --- /dev/null +++ b/src/helpers/ota/BootloaderMigrationPolicy.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include "OtaBootloaderUpdate.h" + +namespace mesh { +namespace ota { +namespace migration { + +static const uint32_t APP_CEILING = 0xED000u; +static const uint32_t BOOT_BASE = 0xF4000u; +static const uint32_t BOOT_SIZE = 0xA000u; +static const uint32_t PARAMS_BASE = 0xFE000u; +static const uint32_t SETTINGS_BASE = 0xFF000u; +static const uint32_t ACL_READ_DENIED = 1u << 2; +static const uint32_t ACL_WRITE_DENIED = 1u << 1; + +struct AclRegion { + uint32_t address; + uint32_t size; + uint32_t permissions; +}; + +struct Snapshot { + uint32_t part; + uint32_t flash_page_size; + uint32_t flash_pages; + uint32_t softdevice_size; + uint16_t softdevice_fwid; + uint32_t app_base; + uint32_t app_ceiling; + uint16_t expected_softdevice_fwid; + uint32_t mbr_boot_override; + uint32_t mbr_params_override; + uint32_t uicr_boot; + uint32_t uicr_params; + uint32_t source; + uint32_t source_size; + uint32_t app_body_size; + uint32_t app_image_size; + AclRegion acl[8]; +}; + +enum class Gate { Ok, Chip, Softdevice, BootAddress, ParamsAddress, Source, Protection }; + +inline bool overlaps(uint32_t start, uint32_t size, uint32_t lo, uint32_t hi) { + return size && uint64_t(start) < hi && uint64_t(start) + size > lo; +} + +inline Gate preflight(const Snapshot &s) { + if (s.part != 0x52840u || s.flash_page_size != 4096u || s.flash_pages != 256u) return Gate::Chip; + if ((s.app_base != 0x26000u && s.app_base != 0x27000u) || + s.app_ceiling > APP_CEILING || s.app_ceiling <= s.app_base || + s.softdevice_size != s.app_base || s.softdevice_fwid != s.expected_softdevice_fwid || + (s.app_base == 0x26000u ? s.softdevice_fwid != 0xB6u : s.softdevice_fwid != 0x123u)) + return Gate::Softdevice; + const uint32_t boot = s.mbr_boot_override == UINT32_MAX ? s.uicr_boot : s.mbr_boot_override; + const uint32_t params = s.mbr_params_override == UINT32_MAX ? s.uicr_params : s.mbr_params_override; + if (boot != BOOT_BASE) return Gate::BootAddress; + if (params != PARAMS_BASE) return Gate::ParamsAddress; + // The source is a linked application resource, never SRAM, external memory, + // a filesystem, the live bootloader, or a dynamically chosen scratch bank. + if (s.source_size != BOOT_SIZE || (s.source & 3u) || s.source < s.app_base || !s.app_body_size || + s.app_body_size > s.app_ceiling - s.app_base || + s.app_image_size != uint64_t(s.app_body_size) + ENDF_LEN || + s.app_image_size > s.app_ceiling - s.app_base || + uint64_t(s.source) + BOOT_SIZE > uint64_t(s.app_base) + s.app_body_size) + return Gate::Source; + for (const auto &r : s.acl) { + // nRF52840 ACL bit 2 denies reads; bit 1 denies writes. COPY_BL also needs its + // parameter page writable. Never try to unlock an existing ACL region. + if ((r.permissions & ACL_READ_DENIED) && + (overlaps(r.address, r.size, s.app_base, s.app_base + s.app_body_size) || + overlaps(r.address, r.size, BOOT_BASE, SETTINGS_BASE))) + return Gate::Protection; + if ((r.permissions & ACL_WRITE_DENIED) && overlaps(r.address, r.size, BOOT_BASE, SETTINGS_BASE)) + return Gate::Protection; + } + return Gate::Ok; +} + +inline bool candidate_matches_resource(const OtaBootloaderIdentity &candidate, + uint32_t board_id, const char name[16], + uint32_t version, uint32_t app_base, uint16_t fwid) { + return ota_bootloader_candidate_identity_canonical(candidate) && + candidate.board_id == board_id && memcmp(candidate.device_name, name, 16) == 0 && + candidate.boot_version == version && + ota_bootloader_continuity_matches_runtime(candidate, 140, fwid, app_base, 1); +} + +inline bool installed_can_migrate(const OtaBootloaderIdentity &installed, + const OtaBootloaderIdentity &candidate, + bool has_legacy, bool legacy_hash_matches, + uint32_t app_base, uint16_t fwid) { + if (!installed.present) + return has_legacy && legacy_hash_matches; + return installed.crc_ok && installed.continuity_present && + ota_bootloader_identity_matches(installed, candidate) && + ota_bootloader_continuity_gate(installed, candidate, candidate.boot_version, + 140, fwid, app_base, 1) == OTA_BOOT_CONTINUITY_OK; +} + +} // namespace migration +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index 99c479745..1e22b79d7 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -1030,6 +1030,26 @@ static __attribute__((noinline)) bool handle_control_command( #endif } else if (is_cmd(a, "bootloader|blupdate", &rest)) { +#if defined(OTA_BOOTLOADER_MIGRATION) + const char* embedded = nullptr; + if (is_cmd(rest, "embedded", &embedded)) { + if (*embedded == 0 || strcmp(embedded, "status") == 0) { + bootloader_migration_status(reply, 160); + } else if (strcmp(embedded, "audit") == 0) { + bootloader_migration_audit(reply, 160); + } else { + const char* confirmation = nullptr; + if (!is_cmd(embedded, "install", &confirmation)) { + strcpy(reply, "ERR usage: ota bootloader embedded [status|audit|install HASH16]"); + } else if (!c.embeddedBootloaderIdle()) { + strcpy(reply, "ERR another OTA operation is active"); + } else { + bootloader_migration_request(confirmation, reply, 160); + } + } + return true; + } +#endif #if defined(NRF52_PLATFORM) && !defined(OTA_SEEDER_ONLY) && \ (defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \ defined(OTA_SD_BOOTLOADER_UPDATE)) diff --git a/src/helpers/ota/OtaContext.h b/src/helpers/ota/OtaContext.h index 51f039271..8c50ac9eb 100644 --- a/src/helpers/ota/OtaContext.h +++ b/src/helpers/ota/OtaContext.h @@ -15,6 +15,9 @@ #include "OtaSelf.h" // ota_self_firmware() - prefer self-describing EndF identity at begin() #include "OtaSelfServePolicy.h" #include "OtaBlInfo.h" // bootloader OTA-apply capability marker (nRF52); cached after first read +#if defined(OTA_BOOTLOADER_MIGRATION) +#include "BootloaderMigration.h" +#endif // Storage policy for the mOTA context. A "dynamic" context is created on demand // and handed back once idle, so its multi-kilobyte workspace only occupies RAM @@ -240,6 +243,18 @@ struct OtaContext { // Apply the COMPLETE fetched .mota (platform dispatch) and arm the slot; sets apply_pending on success // so the deferred-reboot path (mesh loop) takes over. Caller ensures the fetch is COMPLETE. Shared by // manual `ota applydelta` and the auto-install path. +#if defined(OTA_BOOTLOADER_MIGRATION) + bool embeddedBootloaderIdle() const { + const auto state = manager.fetchState(); + return (state == OtaManager::IDLE || state == OtaManager::COMPLETE || state == OtaManager::FAILED) && + !apply_pending && !bootloader_apply_pending && + !manager.pendingServeJobs() && !manager.pendingManifestJobs() +#if defined(OTA_SD_STORE) + && !sdCacheFetching() +#endif + ; + } +#endif bool apply_fetched(char* msg) { return apply_fetched_impl(nullptr, false, msg); } @@ -264,6 +279,11 @@ struct OtaContext { } bool apply_fetched_impl(const uint8_t* rescue_base_hash, bool trusted_auto, char* msg) { +#if defined(OTA_BOOTLOADER_MIGRATION) + if (bootloader_migration_pending()) { + strcpy(msg, "embedded bootloader update is already armed"); return false; + } +#endif // The armed update owns its store and handoff until deferred reboot. // Re-entering an applier can reset that handoff before a later read fails. if (apply_pending || bootloader_apply_pending) { @@ -398,6 +418,11 @@ struct OtaContext { bool apply_fetched_bootloader(const uint8_t operator_mid[4], const uint8_t operator_hash8[8], char* msg) { +#if defined(OTA_BOOTLOADER_MIGRATION) + if (bootloader_migration_pending()) { + strcpy(msg, "embedded bootloader update is already armed"); return false; + } +#endif if (apply_pending || bootloader_apply_pending) { strcpy(msg, "another update is already armed"); return false; diff --git a/test/test_firmware_picker.js b/test/test_firmware_picker.js index 0ccf6ff9b..d3d6bafa9 100644 --- a/test/test_firmware_picker.js +++ b/test/test_firmware_picker.js @@ -46,8 +46,8 @@ for (const hardware of ["WioTrackerL1-1W", "WioTrackerL1Eink"]) { assert.strictEqual(picker.bootloaderForHardware(bootloaderCatalog, hardware, "nrf52").id, "wio_tracker_l1"); } assert(picker.unavailableBootloaderForHardware(bootloaderCatalog, "GAT562_Mesh_Watch13", "nrf52").reason.includes("vibration motor")); -assert(picker.unavailableBootloaderForHardware(bootloaderCatalog, "wio_wm1110", "nrf52").reason.includes("identity and installation route still need qualification")); -const nextBootloaderTag = "v0.11.0-OTAFIX2.4.12"; +assert(picker.unavailableBootloaderForHardware(bootloaderCatalog, "wio_wm1110", "nrf52").reason.includes("not proven native-USB UF2")); +const nextBootloaderTag = "v0.11.0-OTAFIX2.4.13"; const nextBootloaderRelease = { tag_name: nextBootloaderTag, draft: false, prerelease: false, html_url: "https://github.com/" + bootloaderManifest.repository + "/releases/tag/" + nextBootloaderTag, @@ -60,7 +60,7 @@ const nextBootloaderRelease = { }), }; const nextBootloaderCatalog = picker.buildBootloaderCatalog(bootloaderManifest, nextBootloaderRelease); -assert.strictEqual(nextBootloaderCatalog.version, "2.4.12"); +assert.strictEqual(nextBootloaderCatalog.version, "2.4.13"); assert(picker.bootloaderForHardware(nextBootloaderCatalog, "t1000e", "nrf52").files.uf2.url.includes(nextBootloaderTag)); const ambiguousBootloader = structuredClone(nextBootloaderRelease); ambiguousBootloader.assets.push(ambiguousBootloader.assets.find(a => a.name.startsWith("update-heltec_t096_"))); diff --git a/test/test_nrf52_embedded_bootloader.py b/test/test_nrf52_embedded_bootloader.py new file mode 100644 index 000000000..2a4c4f018 --- /dev/null +++ b/test/test_nrf52_embedded_bootloader.py @@ -0,0 +1,343 @@ +"""Offline tests of the embedded bootloader package and build hook.""" +import binascii +import copy +import hashlib +import io +import json +import os +from pathlib import Path +import runpy +import shutil +import struct +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch +import zipfile +import zlib + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) +sys.path.insert(0, str(ROOT / "tools/mota")) +import nrf52_embedded_bootloader as EMBED +import motalib + + +def boot_image(*, v7=False, name=None, board_id=None, storage=None, + boot_version=EMBED.BOOT_VERSION, app_base=None): + """Build the real on-flash envelope, not a mocked identity object.""" + name = name or ("XIAO_DFU" if v7 else "4631_DFU") + board_id = board_id or (0x28860044 if v7 else 0x239A0029) + storage = (0x0E if v7 else 0x0A) if storage is None else storage + image = bytearray(b"\xff" * EMBED.BOOT_SIZE) + struct.pack_into("", + linker="nrf52840_s140_v7.ld" if v7 else "nrf52840_s140_v6.ld") + options.update(kwargs) + return EMBED.emit_resource(self.project, target, self.project / "generated", **options) + + def test_v6_v7_resources_link_and_contain_exact_candidate(self): + compiler = shutil.which("g++") + self.assertIsNotNone(compiler, "host g++ is required to verify the generated resource") + for v7 in (False, True): + with self.subTest(v7=v7): + result = self.emit(v7=v7) + output = self.project / "generated" + image = boot_image(v7=v7) + self.assertEqual((output / "BootloaderMigrationResource.bin").read_bytes(), image) + self.assertEqual(result["candidate_sha256"], hashlib.sha256(image).hexdigest()) + self.assertEqual(result["version"], "2.4.12") + self.assertEqual(result["softdevice_fwid"], 0x123 if v7 else 0xB6) + self.assertEqual(result["app_base"], 0x27000 if v7 else 0x26000) + self.assertEqual(result, json.loads((output / "BootloaderMigrationResource.json").read_text())) + harness = output / "main.cpp" + harness.write_text('''#include "BootloaderMigrationResource.h" +#include +#include +#include +#ifdef _WIN32 +#include +#include +#endif +int main() { +#ifdef _WIN32 + _setmode(_fileno(stdout), _O_BINARY); +#endif + const uint8_t expected_hash[] = {%s}; + if ((uintptr_t)migration_candidate %% 4 || migration_has_legacy || + migration_app_base != %d || migration_softdevice_fwid != %d || + migration_board_id != %d || migration_boot_version != %d || + strcmp(migration_device_name, "%s") || + memcmp(expected_hash, migration_candidate_sha256, 32)) return 1; + for (uint8_t byte : migration_legacy_sha256) if (byte) return 2; + return fwrite(migration_candidate, 1, 40960, stdout) == 40960 ? 0 : 3; +} +''' % (",".join(map(str, hashlib.sha256(image).digest())), result["app_base"], + result["softdevice_fwid"], result["board_id"], EMBED.BOOT_VERSION, result["device_name"])) + executable = output / ("resource.exe" if os.name == "nt" else "resource") + subprocess.run([compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", + str(output / "BootloaderMigrationResource.cpp"), str(harness), + "-o", str(executable)], check=True, capture_output=True) + self.assertEqual(subprocess.check_output([str(executable)]), image) + + def test_manifest_pin_and_release_identity_are_both_required(self): + path = self.project / "docs/_data/bootloader_manifest.json" + path.write_bytes(path.read_bytes() + b" ") + with self.assertRaisesRegex(ValueError, "pinned"): + EMBED.load_manifest(self.project) + self.manifest["version"] = "2.4.11" + self.pin_manifest() + with self.assertRaisesRegex(ValueError, "release identity"): + EMBED.load_manifest(self.project) + + def test_whole_zip_pin_and_internal_crc_are_independent_checks(self): + good = candidate(boot_image()) + with self.assertRaisesRegex(ValueError, "pinned release"): + EMBED.validate_candidate_zip(good[:-1] + bytes([good[-1] ^ 1]), profile(good)) + bad_image = bytearray(boot_image()) + bad_image[0x300] ^= 1 + data = candidate(bytes(bad_image)) # Valid ZIP hash and Legacy CRC, invalid BLMF CRC. + with self.assertRaisesRegex(ValueError, "identity/version/vector/CRC"): + EMBED.validate_candidate_zip(data, profile(data)) + data = candidate(boot_image(), packet_bad=True) + with self.assertRaisesRegex(ValueError, "init metadata/firmware CRC"): + EMBED.validate_candidate_zip(data, profile(data)) + + def test_candidate_version_device_layout_and_capability_must_match(self): + cases = [ + (boot_image(boot_version=0x02040BFF), {}, "identity/version"), + (boot_image(), {"name": "OTHER_DFU"}, "identity/version"), + (boot_image(app_base=0x27000), {}, "layout differs"), + (boot_image(), {"storage": "qspi"}, "capability marker"), + ] + for image, options, message in cases: + with self.subTest(message=message, options=options): + data = candidate(image) + with self.assertRaisesRegex(ValueError, message): + EMBED.validate_candidate_zip(data, profile(data, **options)) + data = candidate(boot_image(v7=True)) + with self.assertRaisesRegex(ValueError, "layout differs"): + EMBED.validate_candidate_zip(data, profile(data, name="XIAO_DFU", storage="qspi")) + + def test_pinned_new_qspi_profile_does_not_expand_signed_inventory(self): + image = boot_image(v7=True, name="FUTURE_DFU", board_id=0x12345678) + data = candidate(image, v7=True) + self.assertIsNone(motalib.bootloader_qualified_storage_profiles(0x12345678, "FUTURE_DFU")) + actual, identity = EMBED.validate_candidate_zip(data, profile(data, v7=True, name="FUTURE_DFU")) + self.assertEqual(actual, image) + self.assertEqual(identity.board_id, 0x12345678) + self.assertIsNone(motalib.bootloader_qualified_storage_profiles(0x12345678, "FUTURE_DFU")) + + def test_zip_member_geometry_is_bounded(self): + for extra in (("unexpected.txt", b"x"), ("../firmware.bin", b"x")): + data = candidate(boot_image(), extras=[extra]) + with self.subTest(extra=extra[0]), self.assertRaisesRegex(ValueError, "exactly one"): + EMBED.validate_candidate_zip(data, profile(data)) + + def test_cached_asset_is_revalidated_and_bad_download_is_not_cached(self): + data = candidate(boot_image()) + item = profile(data) + asset = item["files"]["zip"] + cached = self.cache / (asset["sha256"] + "-" + asset["name"]) + self.assertEqual(EMBED.download_zip(item, self.cache), data) + cached.write_bytes(b"x" * len(data)) + with patch.object(EMBED, "urlopen", return_value=io.BytesIO(data)) as download: + self.assertEqual(EMBED.download_zip(item, self.cache), data) + download.assert_called_once_with(asset["url"], timeout=60) + cached.unlink() + with patch.object(EMBED, "urlopen", return_value=io.BytesIO(b"x" * len(data))): + with self.assertRaisesRegex(ValueError, "size/SHA-256"): + EMBED.download_zip(item, self.cache) + self.assertFalse(cached.exists()) + + def test_effective_ota_flags_and_exact_source_role_fail_closed(self): + for flags in ("-DNRF52_PLATFORM", "-DENABLE_OTA=1", "-DNRF52_PLATFORM -DENABLE_OTA=0", + "-DNRF52_PLATFORM -DENABLE_OTA=1 -UENABLE_OTA", + "-DNRF52_PLATFORM -DENABLE_OTA=1 -DOTA_SEEDER_ONLY=0", + "-DNRF52_PLATFORM -DENABLE_OTA=1 -DDISABLE_LORA_OTA=0"): + with self.subTest(flags=flags), self.assertRaisesRegex(ValueError, "OTA receiver"): + self.emit(flags=flags) + for source in ("", "+<*>", "+<../examples/simple_room_server>", + "+<../examples/simple_repeater> +<../examples/companion_radio>", + "+<../examples/simple_repeater> -<../examples/simple_repeater>"): + with self.subTest(source=source), self.assertRaisesRegex(ValueError, "exact repeater"): + self.emit(src_filter=source) + + def test_profile_role_uniqueness_and_retired_hardware_are_enforced(self): + with self.assertRaisesRegex(ValueError, "limited to repeater"): + EMBED.select_profile(self.manifest, "RAK_4631_companion_radio_full") + ambiguous = copy.deepcopy(self.manifest) + ambiguous["profiles"].append(copy.deepcopy(ambiguous["profiles"][0])) + with self.assertRaisesRegex(ValueError, "one exact"): + EMBED.select_profile(ambiguous, "RAK_4631_repeater") + retired = copy.deepcopy(self.manifest) + retired["unavailableProfiles"] = [{"meshcoreHardware": ["RAK_4631"]}] + with self.assertRaisesRegex(ValueError, "no qualified"): + EMBED.select_profile(retired, "RAK_4631_repeater") + + def test_linker_layout_and_full_legacy_hash_are_required(self): + with self.assertRaisesRegex(ValueError, "application linker"): + self.emit(linker="nrf52840_s140_v7.ld") + linker = self.project / "board.ld" + linker.write_text("MEMORY { FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0x90000 }") + self.assertEqual(EMBED.application_base(self.project, linker), 0x26000) + with self.assertRaisesRegex(ValueError, "cannot verify"): + EMBED.application_base(self.project, "unknown.ld") + legacy = bytes(range(256)) * 160 + path = self.project / "legacy.bin" + path.write_bytes(legacy) + with self.assertRaisesRegex(ValueError, "qualified MeshEnvy"): + EMBED.load_legacy(path) + digest = hashlib.sha256(legacy).hexdigest() + with patch.object(EMBED, "LEGACY_SHA256", digest): + result = self.emit(legacy_path=path) + self.assertEqual(result["legacy_sha256"], digest) + self.assertEqual(EMBED.load_legacy(path), legacy) + path.write_bytes(legacy[:-1] + bytes([legacy[-1] ^ 1])) + with self.assertRaisesRegex(ValueError, "qualified MeshEnvy"): + EMBED.load_legacy(path) + path.write_bytes(legacy[:-1]) + with self.assertRaisesRegex(ValueError, "qualified MeshEnvy"): + EMBED.load_legacy(path) + with self.assertRaisesRegex(ValueError, "restricted to RAK4631"): + self.emit(v7=True, legacy_path=path) + + def test_pio_hook_requires_explicit_version_and_correct_mcu(self): + class Environment(dict): + def __init__(inner): + super().__init__(PROJECT_DIR=str(self.project), PIOENV="RAK_4631_repeater", + BUILD_FLAGS="-DNRF52_PLATFORM -DENABLE_OTA=1") + inner.actions = [] + inner.mcu = "nrf52840" + def BoardConfig(inner): + return {"build.mcu": inner.mcu, "build.ldscript": "nrf52840_s140_v6.ld"} + def subst(inner, value): + self.assertEqual(value, "$BUILD_DIR") + return str(self.project / "hook-build") + def GetProjectOption(inner, key, default): + self.assertEqual(key, "build_src_filter") + return "+<../examples/simple_repeater>" + def AppendUnique(inner, **kwargs): + inner.actions.append(("append", kwargs)) + def BuildSources(inner, *args, **kwargs): + inner.actions.append(("sources", args, kwargs)) + + def execute(environment, **options): + controlled = {key: value for key, value in os.environ.items() if key not in ( + "MESHCORE_NRF52_EMBED_BOOTLOADER", "MESHCORE_NRF52_LEGACY_BOOTLOADER")} + with patch.dict(os.environ, dict(controlled, **options), clear=True): + runpy.run_path(str(ROOT / "scripts/nrf52_embedded_bootloader_pio.py"), + init_globals={"env": environment, "Import": lambda _: None}) + + environment = Environment() + execute(environment) + self.assertEqual(environment.actions, []) + for options in ({"MESHCORE_NRF52_EMBED_BOOTLOADER": "2.4.11"}, + {"MESHCORE_NRF52_LEGACY_BOOTLOADER": "legacy.bin"}): + with self.subTest(options=options), self.assertRaises(RuntimeError): + execute(Environment(), **options) + environment = Environment() + environment.mcu = "nrf52832" + with self.assertRaisesRegex(RuntimeError, "nRF52840"): + execute(environment, MESHCORE_NRF52_EMBED_BOOTLOADER="2.4.12") + environment = Environment() + execute(environment, MESHCORE_NRF52_EMBED_BOOTLOADER="2.4.12") + generated = self.project / "hook-build/bootloader-migration-resource" + self.assertEqual((generated / "BootloaderMigrationResource.bin").read_bytes(), boot_image()) + self.assertEqual(environment.actions[0], ("append", { + "CPPDEFINES": [("OTA_BOOTLOADER_MIGRATION", 1)], "CPPPATH": [str(generated)]})) + self.assertEqual(environment.actions[1], ("sources", (str(generated / "objects"), str(generated)), + {"src_filter": ["+<*.cpp>"]})) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_nrf52_embedded_variant.py b/test/test_nrf52_embedded_variant.py new file mode 100644 index 000000000..b3a93a7d8 --- /dev/null +++ b/test/test_nrf52_embedded_variant.py @@ -0,0 +1,257 @@ +"""Exercise embedded-bootloader build identity and packaged-image gates without PIO.""" + +import argparse +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile +from types import SimpleNamespace +import unittest +from unittest import mock +import zipfile + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) +sys.path.insert(0, str(ROOT / "tools/mota")) +import motalib as ml +import qualify_nrf52_embedded_bootloader as qualifier + +SPEC = importlib.util.spec_from_file_location( + "embedded_variant_recipe", ROOT / "scripts/firmware_build_recipe.py") +recipe = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(recipe) + +TARGET = "RAK_4631_repeater" +VERSION = "v1.18.0.1-halo-keymind-cascade-dev-12345678" + + +class BuildIdentityTests(unittest.TestCase): + def bash(self, script, *arguments): + environment = os.environ.copy() + environment.pop("MESHCORE_NRF52_EMBED_BOOTLOADER", None) + environment.pop("MESHCORE_NRF52_LEGACY_BOOTLOADER", None) + return subprocess.run( + ["bash", "-c", 'source "$1/build_legacy.sh"\n' + 'pio() { echo FORBIDDEN_PLATFORMIO >&2; return 99; }\n' + script, + "embedded-variant", str(ROOT), *arguments], + cwd=ROOT, env=environment, text=True, capture_output=True, timeout=10) + + def test_option_accepts_only_explicit_pinned_release_and_retains_target(self): + result = self.bash(''' +shift +parse_cli_options "$@" || exit $? +printf '%s\n' "$MESHCORE_NRF52_EMBED_BOOTLOADER" "${PARSED_COMMAND_ARGS[@]}" +''', "build-firmware", TARGET, "--with-bootloader", "2.4.12") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(result.stdout.splitlines(), ["2.4.12", "build-firmware", TARGET]) + for arguments in (("--with-bootloader",), + ("--with-bootloader", "2.4.11"), + ("--with-bootloader", "latest")): + with self.subTest(arguments=arguments): + result = self.bash('shift\nparse_cli_options "$@"\n', *arguments) + self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("pinned release 2.4.12", result.stdout + result.stderr) + + def filename(self, *, embedded=False, ota=True, infix="", full=False): + return self.bash(''' +PIO_ENV_PLATFORM_BY_NAME[RAK_4631_repeater]=NRF52_PLATFORM +PIO_ENV_OTA_BY_NAME[RAK_4631_repeater]=$3 +BUILD_PROFILE_FOR_TARGET=auto +FIRMWARE_FILENAME_INFIX=$4 +ESP32_FULL_BUILD=$5 +if [ "$2" = yes ]; then export MESHCORE_NRF52_EMBED_BOOTLOADER=2.4.12; fi +embedded_bootloader_artifact_target RAK_4631_repeater || exit $? +get_firmware_filename RAK_4631_repeater "$6" +''', "yes" if embedded else "no", "1" if ota else "0", infix, + "1" if full else "0", VERSION) + + def test_ordinary_names_and_ota_classification_survive_publication_suffix(self): + for embedded in (False, True): + for ota in (False, True): + with self.subTest(embedded=embedded, ota=ota): + result = self.filename(embedded=embedded, ota=ota) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + artifact = TARGET + ("-with-bootloader-2.4.12" if embedded else "") + filename = artifact + ("-ota" if ota else "") + "-" + VERSION + self.assertEqual(result.stdout.splitlines(), [artifact, filename]) + + def test_full_ota_profile_keeps_both_publication_markers(self): + for embedded in (False, True): + for infix, full in (("full-ota", False), ("full", True), + ("full-logging", True), ("full-usb-wifi", True)): + with self.subTest(embedded=embedded, infix=infix, full=full): + result = self.filename(embedded=embedded, infix=infix, full=full) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + artifact = TARGET + ("-with-bootloader-2.4.12" if embedded else "") + expected_infix = infix if infix.endswith("-ota") else infix + "-ota" + self.assertEqual(result.stdout.splitlines(), + [artifact, artifact + "-" + expected_infix + "-" + VERSION]) + + def test_invalid_inherited_release_cannot_create_publishable_name(self): + result = self.bash(''' +export MESHCORE_NRF52_EMBED_BOOTLOADER=2.4.11 +embedded_bootloader_artifact_target RAK_4631_repeater +''') + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, "") + + +class PackagedApplicationTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="mesh-embedded-package-") + self.addCleanup(self.temporary.cleanup) + self.directory = Path(self.temporary.name) + self.resource = self.directory / "bootloader-migration-resource" + self.resource.mkdir() + self.manifest_path = self.directory / "firmware.capabilities.json" + self.manifest = {"platform": "NRF52_PLATFORM", "verified": True, + "target": TARGET, + "artifact_target": TARGET + "-with-bootloader-2.4.12", + "capabilities": ["ota.update.lora", "bootloader.embedded"], + "source_commit": "a" * 40} + self.manifest_path.write_text(json.dumps(self.manifest)) + self.candidate = (struct.pack(" +#include +#include +#include "helpers/ota/BootloaderMigrationPolicy.h" +using namespace mesh::ota; +using namespace mesh::ota::migration; +static void wr16(uint8_t *p, uint16_t n) { p[0]=n; p[1]=n>>8; } +static void wr32(uint8_t *p, uint32_t n) { wr16(p,n); wr16(p+2,n>>16); } +static OtaBootloaderIdentity identity(uint32_t version, uint32_t base=0x26000, uint16_t fwid=0xB6) { + std::array bytes; + bytes.fill(0xFF); + uint8_t *p=bytes.data()+OTA_BOOT_CANDIDATE_MANIFEST_OFFSET; + wr32(p,OTA_BOOT_MANIFEST_MAGIC0); wr32(p+4,OTA_BOOT_MANIFEST_MAGIC1); + wr16(p+8,1); wr16(p+10,44); wr32(p+12,BOOT_BASE); wr32(p+16,BOOT_SIZE); + wr32(p+20,0x239A0029); std::memset(p+24,0,16); std::memcpy(p+24,"4631_DFU",8); + wr32(p+44,OTA_BOOT_CONTINUITY_MAGIC0); wr32(p+48,OTA_BOOT_CONTINUITY_MAGIC1); + wr16(p+52,2); wr16(p+54,32); wr32(p+56,version); wr16(p+60,140); + wr16(p+62,fwid); wr32(p+64,base); wr16(p+68,1); wr16(p+70,0); wr32(p+72,0); + wr32(p+40,ota_boot_image_crc32(bytes.data(),bytes.size(),OTA_BOOT_CANDIDATE_MANIFEST_OFFSET+40)); + OtaBootloaderIdentity out; + assert(ota_bootloader_candidate_identity_from_image(bytes.data(),bytes.size(),out)); + bytes[10]^=1; + OtaBootloaderIdentity corrupt; + assert(!ota_bootloader_candidate_identity_from_image(bytes.data(),bytes.size(),corrupt)); + return out; +} +static Snapshot good() { + Snapshot s{}; + s.part=0x52840; s.flash_page_size=4096; s.flash_pages=256; + s.app_base=0x26000; s.app_ceiling=0xED000; + s.softdevice_size=s.app_base; s.softdevice_fwid=s.expected_softdevice_fwid=0xB6; + s.mbr_boot_override=s.mbr_params_override=UINT32_MAX; + s.uicr_boot=BOOT_BASE; s.uicr_params=PARAMS_BASE; + s.source=0xA0000; s.source_size=BOOT_SIZE; s.app_body_size=0x90000; + s.app_image_size=s.app_body_size+ENDF_LEN; + return s; +} +int main(int argc,char **argv) { + assert(argc==2); + if (!std::strcmp(argv[1],"geometry")) { + assert(preflight(good())==Gate::Ok); + Snapshot s=good(); s.part=0x52833; assert(preflight(s)==Gate::Chip); + s=good(); s.flash_page_size=2048; assert(preflight(s)==Gate::Chip); + s=good(); s.flash_pages=512; assert(preflight(s)==Gate::Chip); + s=good(); s.app_base=s.softdevice_size=0x27000; + s.softdevice_fwid=s.expected_softdevice_fwid=0x123; assert(preflight(s)==Gate::Ok); + s.expected_softdevice_fwid=0xB6; assert(preflight(s)==Gate::Softdevice); + s=good(); s.softdevice_size=0x27000; assert(preflight(s)==Gate::Softdevice); + s=good(); s.app_ceiling=0xEE000; assert(preflight(s)==Gate::Softdevice); + s=good(); s.app_ceiling=s.app_base; assert(preflight(s)==Gate::Softdevice); + s=good(); s.uicr_boot=0xF0000; assert(preflight(s)==Gate::BootAddress); + s=good(); s.mbr_boot_override=BOOT_BASE; s.uicr_boot=0; assert(preflight(s)==Gate::Ok); + s=good(); s.mbr_boot_override=0xF0000; assert(preflight(s)==Gate::BootAddress); + s=good(); s.uicr_params=UINT32_MAX; assert(preflight(s)==Gate::ParamsAddress); + s=good(); s.mbr_params_override=SETTINGS_BASE; assert(preflight(s)==Gate::ParamsAddress); + const uint32_t bad[]={0,0x1000,0x25FFC,0xED000,BOOT_BASE,PARAMS_BASE,0x20000000,UINT32_MAX-3}; + for (uint32_t address:bad) { s=good(); s.source=address; assert(preflight(s)==Gate::Source); } + s=good(); ++s.source; assert(preflight(s)==Gate::Source); + s=good(); --s.source_size; assert(preflight(s)==Gate::Source); + s=good(); s.app_body_size=s.source-s.app_base+BOOT_SIZE-1; assert(preflight(s)==Gate::Source); + s=good(); s.app_body_size=s.app_ceiling-s.app_base+1; assert(preflight(s)==Gate::Source); + s=good(); s.app_body_size=s.app_ceiling-s.app_base; s.app_image_size=s.app_body_size+ENDF_LEN; + assert(preflight(s)==Gate::Source); // EndF must fit as well as the body. + s=good(); --s.app_image_size; assert(preflight(s)==Gate::Source); + s=good(); s.app_ceiling=0xD4000; s.app_body_size=0xAF000; assert(preflight(s)==Gate::Source); + } else if (!std::strcmp(argv[1],"acl")) { + for (unsigned slot=0;slot<8;++slot) { + for (uint32_t address=BOOT_BASE;address + +uint32_t millis(); diff --git a/tools/mota/tests/migration_stubs/BootloaderMigrationResource.h b/tools/mota/tests/migration_stubs/BootloaderMigrationResource.h new file mode 100644 index 000000000..787477b37 --- /dev/null +++ b/tools/mota/tests/migration_stubs/BootloaderMigrationResource.h @@ -0,0 +1,10 @@ +#pragma once +#include + +extern const uint8_t migration_candidate[40960]; +extern const uint8_t migration_candidate_sha256[32]; +extern const uint8_t migration_legacy_sha256[32]; +extern const bool migration_has_legacy; +extern const uint32_t migration_app_base, migration_boot_version, migration_board_id; +extern const uint16_t migration_softdevice_fwid; +extern const char migration_device_name[16]; diff --git a/tools/mota/tests/migration_stubs/SHA256.h b/tools/mota/tests/migration_stubs/SHA256.h new file mode 100644 index 000000000..de1123a87 --- /dev/null +++ b/tools/mota/tests/migration_stubs/SHA256.h @@ -0,0 +1,11 @@ +#pragma once +#include + +// Scheduling tests control the digest to exercise the no-op branch, then +// stop at the EndF lookup before any destructive hardware operation. +class SHA256 { +public: + void reset(); + void update(const void *, size_t); + void finalize(void *, size_t); +}; diff --git a/tools/mota/tests/migration_stubs/Utils.h b/tools/mota/tests/migration_stubs/Utils.h new file mode 100644 index 000000000..637e185a6 --- /dev/null +++ b/tools/mota/tests/migration_stubs/Utils.h @@ -0,0 +1,10 @@ +#pragma once +#include +#include +namespace mesh { +class Utils { +public: + static void sha256(uint8_t *, size_t, const uint8_t *, int); + static void sha256(uint8_t *, size_t, const uint8_t *, int, const uint8_t *, int); +}; +} diff --git a/tools/mota/tests/migration_stubs/nrf.h b/tools/mota/tests/migration_stubs/nrf.h new file mode 100644 index 000000000..5e749bc8d --- /dev/null +++ b/tools/mota/tests/migration_stubs/nrf.h @@ -0,0 +1,35 @@ +#pragma once +#include + +#define ACL_ACL_PERM_READ_Msk 4u +#define ACL_ACL_PERM_WRITE_Msk 2u +#define NRF_SUCCESS 0u + +struct MigrationTestPower { + uint32_t GPREGRET, GPREGRET2; +}; +struct MigrationTestFicr { + struct { + uint32_t PART; + } INFO; + uint32_t CODEPAGESIZE, CODESIZE; +}; +struct MigrationTestAcl { + struct { + uint32_t ADDR, SIZE, PERM; + } ACL[8]; +}; +struct MigrationTestUicr { + uint32_t NRFFW[2]; +}; +struct MigrationTestWdt { + uint32_t RUNSTATUS, CRV, RREN, RR[8]; +}; + +extern MigrationTestPower *NRF_POWER; +extern MigrationTestFicr *NRF_FICR; +extern MigrationTestAcl *NRF_ACL; +extern MigrationTestUicr *NRF_UICR; +extern MigrationTestWdt *NRF_WDT; + +void NVIC_SystemReset(); diff --git a/tools/mota/tests/migration_stubs/nrf_mbr.h b/tools/mota/tests/migration_stubs/nrf_mbr.h new file mode 100644 index 000000000..479a62a4d --- /dev/null +++ b/tools/mota/tests/migration_stubs/nrf_mbr.h @@ -0,0 +1,15 @@ +#pragma once +#include + +#define SD_MBR_COMMAND 0x18 +#define SD_MBR_COMMAND_COPY_BL 1u + +struct sd_mbr_command_t { + uint32_t command; + union { + struct { + uint32_t *bl_src; + uint32_t bl_len; + } copy_bl; + } params; +}; diff --git a/tools/mota/tests/migration_stubs/nrf_sdm.h b/tools/mota/tests/migration_stubs/nrf_sdm.h new file mode 100644 index 000000000..c75dcef0e --- /dev/null +++ b/tools/mota/tests/migration_stubs/nrf_sdm.h @@ -0,0 +1,5 @@ +#pragma once +#include + +uint32_t sd_softdevice_is_enabled(uint8_t *enabled); +uint32_t sd_softdevice_disable(); diff --git a/tools/mota/tests/migration_stubs/nrf_soc.h b/tools/mota/tests/migration_stubs/nrf_soc.h new file mode 100644 index 000000000..d484aee6e --- /dev/null +++ b/tools/mota/tests/migration_stubs/nrf_soc.h @@ -0,0 +1,4 @@ +#pragma once +#include + +uint32_t sd_power_gpregret_get(uint32_t index, uint32_t *result); diff --git a/tools/mota/tests/migration_stubs/runtime.cpp b/tools/mota/tests/migration_stubs/runtime.cpp new file mode 100644 index 000000000..37584c498 --- /dev/null +++ b/tools/mota/tests/migration_stubs/runtime.cpp @@ -0,0 +1,261 @@ +#include "Arduino.h" +#include "BootloaderMigrationResource.h" +#include "SHA256.h" +#include "helpers/ota/OtaSelf.h" +#include "nrf.h" +#include "nrf_mbr.h" +#include "nrf_sdm.h" +#include "nrf_soc.h" + +#include +#include +#include +#include +#include +#if defined(_WIN32) +#include +#else +#include +#endif + +namespace migration_test { +uint32_t now = 0; +unsigned endf_checks = 0; +unsigned softdevice_checks = 0; +unsigned disables = 0; +unsigned resets = 0; +unsigned copies = 0; +bool candidate_installed = false; + +void mbr_command() { + ++copies; + throw std::runtime_error("unexpected MBR command in state-machine test"); +} +} // namespace migration_test + +// Include the actual driver. Only its target-specific naked SVC adapter is +// replaced: function-like 'volatile(...)' captures the extended assembly, but +// leaves ordinary 'volatile uint8_t *' memory reads unchanged. The adapter is +// an explicit failing trap, never a successful simulated flash operation. +#define __attribute__(...) +#define __asm +#define volatile(...) migration_test::mbr_command() +#include "helpers/ota/BootloaderMigration.cpp" +#undef volatile +#undef __asm +#undef __attribute__ + +const uint8_t migration_candidate[40960] = {}; +const uint8_t migration_candidate_sha256[32] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF }; +const uint8_t migration_legacy_sha256[32] = {}; +const bool migration_has_legacy = false; +const uint32_t migration_app_base = 0x26000, migration_boot_version = 0x02040CFF, + migration_board_id = 0x239A0029; +const uint16_t migration_softdevice_fwid = 0xB6; +const char migration_device_name[16] = "4631_DFU"; +namespace mesh { namespace ota { +extern "C" { +uint32_t __flash_arduino_start[1] = {}; +uint32_t __flash_arduino_end[1] = {}; +} +} } + +MigrationTestPower power_registers = {}; +MigrationTestPower *NRF_POWER = &power_registers; +MigrationTestFicr *NRF_FICR = nullptr; +MigrationTestAcl *NRF_ACL = nullptr; +MigrationTestUicr *NRF_UICR = nullptr; +MigrationTestWdt *NRF_WDT = nullptr; + +uint32_t millis() { + return migration_test::now; +} + +void NVIC_SystemReset() { + ++migration_test::resets; + throw std::runtime_error("unexpected reset in state-machine test"); +} + +uint32_t sd_softdevice_is_enabled(uint8_t *enabled) { + ++migration_test::softdevice_checks; + *enabled = 0; + return NRF_SUCCESS; +} + +uint32_t sd_softdevice_disable() { + ++migration_test::disables; + throw std::runtime_error("unexpected SoftDevice disable in state-machine test"); +} + +uint32_t sd_power_gpregret_get(uint32_t, uint32_t *result) { + *result = 0; + return NRF_SUCCESS; +} + +void SHA256::reset() { +} +void SHA256::update(const void *, size_t) { +} +void SHA256::finalize(void *out, size_t size) { + assert(size == 32); + // Scheduling tests exercise the full memory-read loop; cryptography itself + // is the production SHA256 library's contract, not this hardware stub. + std::memset(out, 0, size); + if (migration_test::candidate_installed) + std::memcpy(out, migration_candidate_sha256, size); +} + +namespace mesh { +namespace ota { +bool ota_self_firmware(SelfFwInfo &) { + ++migration_test::endf_checks; + return false; // Stop at the first install preflight, before hardware access. +} +} // namespace ota +} // namespace mesh + +namespace { +using namespace mesh::ota; +const char confirmation[] = "0123456789ABCDEF"; +char reply[160]; + +void arm() { + assert(bootloader_migration_request(confirmation, reply, sizeof(reply))); + assert(bootloader_migration_pending()); + assert(std::strstr(reply, "OK migration armed")); +} + +void no_install() { + assert(migration_test::endf_checks == 0); + assert(migration_test::softdevice_checks == 0); + assert(migration_test::disables == 0); + assert(migration_test::resets == 0); + assert(migration_test::copies == 0); +} + +void status_result(unsigned result) { + bootloader_migration_status(reply, sizeof(reply)); + char expected[24]; + std::snprintf(expected, sizeof(expected), "last=%u", result); + assert(std::strstr(reply, expected)); +} +} // namespace + +int main(int argc, char **argv) { + assert(argc == 2); + using namespace migration_test; +#if defined(_WIN32) + assert(VirtualAlloc(reinterpret_cast(0xF0000), 0x10000, + MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) != nullptr); +#else + assert(mmap(reinterpret_cast(0xF0000), 0x10000, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED); +#endif + const char *scenario = argv[1]; + if (std::strcmp(scenario, "already-installed") == 0) { + candidate_installed = true; + assert(bootloader_migration_request(confirmation, reply, sizeof(reply))); + assert(!bootloader_migration_pending()); + assert(std::strstr(reply, "already installed")); + now = 2000; + bootloader_migration_tick(true, true); + no_install(); + bootloader_migration_status(reply, sizeof(reply)); + assert(std::strstr(reply, "already installed")); + } else if (std::strcmp(scenario, "wrong-confirmation") == 0) { + assert(!bootloader_migration_request("0123456789ABCDE0", reply, sizeof(reply))); + assert(!bootloader_migration_request("0123456789abcdef", reply, sizeof(reply))); + assert(!bootloader_migration_request("0123456789ABCDEF ", reply, sizeof(reply))); + assert(!bootloader_migration_pending()); + now = 2000; + bootloader_migration_tick(true, true); + no_install(); + } else if (std::strcmp(scenario, "grace-and-drain") == 0) { + arm(); + now = 1499; + bootloader_migration_tick(true, true); + no_install(); + now = 1500; + bootloader_migration_tick(false, true); + no_install(); + now = 29999; + bootloader_migration_tick(false, true); + no_install(); + assert(bootloader_migration_pending()); + bootloader_migration_tick(true, true); + assert(endf_checks == 1); + assert(!bootloader_migration_pending()); + assert(disables == 0 && resets == 0 && copies == 0); + status_result(1); + } else if (std::strcmp(scenario, "duplicate-arm") == 0) { + arm(); + now = 1499; + assert(!bootloader_migration_request(confirmation, reply, sizeof(reply))); + assert(std::strstr(reply, "already armed")); + now = 1500; + bootloader_migration_tick(true, true); + assert(endf_checks == 1); // A duplicate must not extend the grace period. + } else if (std::strcmp(scenario, "timeout") == 0) { + arm(); + now = 30000; + bootloader_migration_tick(true, true); + assert(!bootloader_migration_pending()); + no_install(); + status_result(15); + bootloader_migration_tick(true, true); + assert(endf_checks == 0); + } else if (std::strcmp(scenario, "ota-cancel-rearm") == 0) { + arm(); + now = 100; + bootloader_migration_tick(false, false); + assert(!bootloader_migration_pending()); + no_install(); + status_result(16); + now = 2000; + bootloader_migration_tick(true, true); + assert(endf_checks == 0); // Becoming idle must not revive cancelled work. + arm(); + now = 3499; + bootloader_migration_tick(true, true); + assert(endf_checks == 0); + now = 3500; + bootloader_migration_tick(true, true); + assert(endf_checks == 1); + } else if (std::strcmp(scenario, "ota-cancel-at-install") == 0) { + arm(); + now = 1500; + bootloader_migration_tick(true, false); + assert(!bootloader_migration_pending()); + no_install(); + status_result(16); + } else if (std::strcmp(scenario, "stale-retained-result") == 0) { + power_registers.GPREGRET = 0xCC; + status_result(12); + arm(); + status_result(0); + now = 30000; + bootloader_migration_tick(true, true); + assert(!bootloader_migration_pending()); + status_result(15); + arm(); + status_result(0); + ++now; + bootloader_migration_tick(true, false); + assert(!bootloader_migration_pending()); + status_result(16); + assert(power_registers.GPREGRET == 0xCC); + assert(endf_checks == 0); + } else if (std::strcmp(scenario, "clock-wrap") == 0) { + now = UINT32_MAX - 999u; + arm(); + now += 1499u; + bootloader_migration_tick(true, true); + no_install(); + ++now; + bootloader_migration_tick(true, true); + assert(endf_checks == 1); + } else { + throw std::runtime_error("unknown scenario"); + } + assert(disables == 0 && resets == 0 && copies == 0); +}