From 774c2adc800d949d0fdcde7ee3805a288ce4e61d Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 16 Oct 2026 01:18:26 -0700 Subject: [PATCH] Add adaptive RAK4631 SD storage to full firmware profiles --- .github/workflows/run-unit-tests.yml | 5 + build_legacy.sh | 92 +- docs/cli_commands.md | 50 +- docs/companion_radio_full.md | 36 +- docs/ota_nrf52_qspi.md | 121 +++ examples/companion_radio/MyMesh.cpp | 10 + examples/companion_radio/main.cpp | 51 ++ examples/simple_repeater/main.cpp | 26 + examples/simple_room_server/main.cpp | 48 + platformio.ini | 28 + scripts/check_firmware_ram.py | 18 +- src/helpers/StorageLayout.cpp | 21 +- src/helpers/UserGpioPinPolicy.h | 7 + .../RAK13800/RAK13800EthernetInterface.cpp | 119 ++- .../RAK13800/RAK13800EthernetInterface.h | 24 + src/helpers/nrf52/EthernetCLI.h | 33 +- src/helpers/nrf52/EthernetCliRuntime.h | 68 +- src/helpers/nrf52/Rak4631SharedSpi.h | 35 +- src/helpers/ota/OtaApply.cpp | 45 +- src/helpers/ota/OtaApply.h | 4 + src/helpers/ota/OtaBlInfo.h | 42 +- src/helpers/ota/OtaBootloaderUpdate.h | 3 +- src/helpers/ota/OtaCacheSdNrf52.cpp | 43 +- src/helpers/ota/OtaCacheSdNrf52.h | 43 +- src/helpers/ota/OtaCli.cpp | 83 +- src/helpers/ota/OtaContext.h | 110 ++- src/helpers/ota/OtaSdSpiBusNrf52.h | 29 +- src/helpers/ota/OtaSdTypesNrf52.h | 30 + src/helpers/ota/OtaStoreAdaptiveNrf52.h | 123 ++- src/helpers/ota/OtaStoreQspiNrf52.cpp | 36 +- src/helpers/ota/OtaStoreQspiNrf52.h | 7 +- src/helpers/ota/OtaStoreSdNrf52.cpp | 149 +++- src/helpers/ota/OtaStoreSdNrf52.h | 18 +- src/helpers/ota/OtaTargets.h | 819 ++++++++++++------ .../sensors/EnvironmentSensorManager.cpp | 2 +- test/fixtures/ota_sd_cache/mocks/SdFat.h | 11 + test/fixtures/ota_sd_cache/test.cpp | 10 +- test/test_ethernet_terminal_shutdown.py | 2 + test/test_firmware_ram.py | 42 + test/test_nrf52_endf_hex.py | 41 + test/test_ota_sd_cache.py | 8 + test/test_ota_target_names.py | 20 +- test/test_rak15002_sd_seeder.py | 32 +- test/test_rak15002_shared_spi.py | 458 ++++++++++ test/test_rak4631_rak15002_sd_recipes.py | 341 ++++++++ test/test_rak_combined_ota.py | 2 +- test/test_rak_sd_context_handoff.py | 183 ++++ test/test_rak_sd_receiver.py | 535 ++++++++++++ test/test_rak_sd_room_ethernet.py | 211 +++++ tools/mota/gen_targets.py | 53 +- .../nrf52_internal_bootloader_targets.txt | 8 +- tools/mota/pio_endf.py | 12 +- tools/mota/test_mota.py | 29 +- variants/rak4631/platformio.ini | 63 +- 54 files changed, 3962 insertions(+), 477 deletions(-) create mode 100644 src/helpers/ota/OtaSdTypesNrf52.h create mode 100644 test/test_rak15002_shared_spi.py create mode 100644 test/test_rak4631_rak15002_sd_recipes.py create mode 100644 test/test_rak_sd_context_handoff.py create mode 100644 test/test_rak_sd_receiver.py create mode 100644 test/test_rak_sd_room_ethernet.py diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index f343abed2..613af501e 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -722,6 +722,10 @@ jobs: python3 -B test/test_rak_ethernet_transmit.py -v python3 -B test/test_rak4631_ethernet_runtime.py -v python3 -B test/test_ethernet_terminal_shutdown.py -v + python3 -B test/test_rak_sd_receiver.py -v + python3 -B test/test_rak15002_shared_spi.py -v + python3 -B test/test_rak_sd_context_handoff.py -v + python3 -B test/test_rak_sd_room_ethernet.py -v - name: Verify nRF52 infrastructure trims and unchanged Companion scope run: | @@ -729,6 +733,7 @@ jobs: python3 -B test/test_nrf52_tft_font_trim.py -v python3 -B test/test_ota_sd_cache.py -v python3 -B test/test_rak15002_sd_seeder.py -v + python3 -B test/test_rak4631_rak15002_sd_recipes.py -v python3 -B test/test_ota_target_names.py -v python3 -B test/test_ota_cli_listing.py -v diff --git a/build_legacy.sh b/build_legacy.sh index 93ad57ed7..956c60ddc 100755 --- a/build_legacy.sh +++ b/build_legacy.sh @@ -285,6 +285,7 @@ for section, options in data: ota_enabled = False ota_disabled = False sd_ota = False + rak15002_sd_store = False qspi_ota = False admin_enabled = False espnow_enabled = "bridge_espnow" in env_name.lower() @@ -317,11 +318,19 @@ for section, options in data: ota_disabled = True if "OTA_SD_STORE" in str(flag): sd_ota = True + if "OTA_RAK15002_SD_STORE" in str(flag): + rak15002_sd_store = True if "OTA_QSPI_STORE" in str(flag): qspi_ota = True match = pattern.search(str(flag)) if match and platform is None: platform = match.group(0) + # RAK15002 STORE is an optional member of the adaptive RAK recipe, not an + # SD-primary image. Keep the old CACHE-only and all other SD profiles in + # the SD-primary bucket so their release/bootloader safeguards remain + # unchanged. + if rak15002_sd_store: + sd_ota = False if platform: full_enabled = platform == "ESP32_PLATFORM" and ( mqtt_enabled or espnow_enabled or admin_enabled @@ -2917,6 +2926,31 @@ is_nrf52_companion_radio_full_target() { && is_companion_radio_full_target "$1" } +# The RAK15002 STORE path is deliberately limited to the canonical RAK4631 +# receiver identities and the RAK4631 Full Companion. Exact NOR targets, +# deployed bridge identities, ordinary non-OTA roles, and the older strict +# CACHE-only RAK15002 seeder keep their existing storage contracts. +is_rak4631_rak15002_sd_store_target() { + case "${1,,}" in + rak_4631_repeater_unified_lora_ota|\ + rak_4631_repeater_lora_ota_no_external_sensors|\ + rak_4631_room_server_lora_ota_no_external_sensors|\ + rak_4631_room_server_ethernet_lora_ota_no_external_sensors|\ + rak_4631_sensor_lora_ota_no_external_sensors|\ + rak_4631_companion_radio_full) + return 0 + ;; + *) + return 1 + ;; + esac +} + +is_rak4631_rak15002_sd_store_receiver_target() { + is_rak4631_rak15002_sd_store_target "$1" \ + && [ "${1,,}" != "rak_4631_companion_radio_full" ] +} + requires_esp32_arduino3_framework() { local env_name=$1 local pio_env_name=$2 @@ -3938,6 +3972,29 @@ apply_lora_ota_no_external_sensors_profile() { fi } +# Generated RAK4631 room-server and sensor OTA identities compile an ordinary +# role environment, so their storage recipe cannot live only in a PlatformIO +# stanza. Apply the same bounded RAK15002 STORE contract as the explicit +# repeater receiver, after reduced-sensor flags have been resolved. The +# Companion remains source-only; receiver roles retain the adaptive +# internal/NOR path as well as optional SD. +apply_rak4631_rak15002_sd_store_profile() { + local env_name=$1 + local auto_store_flags="-DOTA_RAK_AUTO_STORE=1 -DOTA_QSPI_STORE=1 -DOTA_FLASH_STORE=1 -DOTA_QSPI_SHARED_WISBLOCK_SPI=1 -DOTA_QSPI_SCK_ARDUINO_PIN=3 -DOTA_QSPI_CS_ARDUINO_PIN=31 -DOTA_QSPI_IO0_ARDUINO_PIN=30 -DOTA_QSPI_IO1_ARDUINO_PIN=29 -DOTA_QSPI_IO2_NOT_CONNECTED=1 -DOTA_QSPI_IO3_NOT_CONNECTED=1 -DOTA_QSPI_SCK_FREQUENCY=NRF_QSPI_FREQ_32MDIV4 -DOTA_QSPI_EXPECTED_SIZE=2097152UL" + local sd_flags="-DOTA_RAK15002_SD_STORE=1 -DOTA_SD_STORE=1 -DOTA_SD_CS_PIN=26 -DOTA_SD_SCK_MHZ=4 -DOTA_SD_CACHE_MAX=16 -DOTA_MAX_SERVE=24" + + [ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" = "NRF52_PLATFORM" ] || return 0 + is_rak4631_rak15002_sd_store_target "$env_name" || return 0 + + if is_rak4631_rak15002_sd_store_receiver_target "$env_name"; then + # A receiver may select its existing internal flash or qualified NOR + # backend when no card is present. The runtime owns the shared slot and + # never treats RAK15002 as a generic SD-primary bootloader store. + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} ${auto_store_flags}" + fi + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} ${sd_flags}" +} + append_platformio_build_unflags() { local flags=$1 local flag @@ -4073,14 +4130,23 @@ apply_nrf52_lora_ota_build_recipe() { # app-preserving bootloader staging. Keep this list narrower than generic nRF52 # OTA support: external QSPI/SD, ExtraFS Companions, Ethernet, and # unqualified/full-sensor roles must not advertise this privileged path. The -# same inventory is consumed by the nrf52_base pre-script so direct explicit-env -# and build.sh release builds cannot silently differ. +# listed RAK4631 adaptive receiver aliases are the one intentional exception: +# their STORE overlay is applied after this decision, but still requires the +# same fixed 64 KiB hybrid-RAM / internal-bootloader-update contract. The same +# inventory is consumed by the nrf52_base pre-script so direct explicit-env and +# build.sh release builds cannot silently differ. supports_nrf52_internal_bootloader_update() { [ "${PIO_ENV_PLATFORM_BY_NAME[$1]:-}" = "NRF52_PLATFORM" ] || return 1 - if [ "${PIO_ENV_QSPI_OTA_BY_NAME[$1]:-0}" != "0" ]; then - pio_env_option_contains "$1" build_flags "OTA_RAK_AUTO_STORE" || return 1 + local rak15002_receiver=0 + if is_rak4631_rak15002_sd_store_receiver_target "$1"; then + rak15002_receiver=1 fi - [ "${PIO_ENV_SD_OTA_BY_NAME[$1]:-0}" = "0" ] || return 1 + if [ "${PIO_ENV_QSPI_OTA_BY_NAME[$1]:-0}" != "0" ]; then + pio_env_option_contains "$1" build_flags "OTA_RAK_AUTO_STORE" \ + || [ "$rak15002_receiver" = "1" ] || return 1 + fi + [ "${PIO_ENV_SD_OTA_BY_NAME[$1]:-0}" = "0" ] \ + || [ "$rak15002_receiver" = "1" ] || return 1 is_lora_ota_build "$1" || return 1 grep -Fqx -- "$1" tools/mota/nrf52_internal_bootloader_targets.txt } @@ -4169,10 +4235,17 @@ apply_companion_radio_full_profile() { # Every full Companion is a LoRa mOTA source, never an update destination. # Remove inherited staging/install stores before adding the platform's host - # folder transport. Full also restores WebConfig when a constrained legacy - # WiFi sibling disabled it only to fit its smaller application partition. - append_platformio_build_unflags "-UENABLE_OTA -DOTA_FLASH_STORE=1 -DOTA_SD_STORE=1 -DDISABLE_LORA_OTA=1 -DWEBCONFIG_DISABLED=1" - export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -UOTA_FLASH_STORE -UOTA_SD_STORE -UWEBCONFIG_DISABLED -DOTA_SEEDER_ONLY=1 -DMOTA_TARGET_ID=0 -DCOMPANION_RADIO_FULL=1 -DCOMPANION_FEATURE_TEMP_RADIO=1 -DCOMPANION_FEATURE_OTA_CLI=1 -DENABLE_USB_INTERFACE=1 -DBLE_PIN_CODE=123456 -DMESH_DEBUG=1 -DMESH_PACKET_LOGGING=1" + # folder transport. The RAK4631 Full has an optional RAK15002 archive/cache: + # retain only that STORE while still removing its internal install store. + # Full also restores WebConfig when a constrained legacy WiFi sibling + # disabled it only to fit its smaller application partition. + if is_rak4631_rak15002_sd_store_target "$env_name"; then + append_platformio_build_unflags "-UENABLE_OTA -DOTA_FLASH_STORE=1 -DDISABLE_LORA_OTA=1 -DWEBCONFIG_DISABLED=1" + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -UOTA_FLASH_STORE -DOTA_SD_STORE=1 -UWEBCONFIG_DISABLED -DOTA_SEEDER_ONLY=1 -DMOTA_TARGET_ID=0 -DCOMPANION_RADIO_FULL=1 -DCOMPANION_FEATURE_TEMP_RADIO=1 -DCOMPANION_FEATURE_OTA_CLI=1 -DENABLE_USB_INTERFACE=1 -DBLE_PIN_CODE=123456 -DMESH_DEBUG=1 -DMESH_PACKET_LOGGING=1" + else + append_platformio_build_unflags "-UENABLE_OTA -DOTA_FLASH_STORE=1 -DOTA_SD_STORE=1 -DDISABLE_LORA_OTA=1 -DWEBCONFIG_DISABLED=1" + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -UOTA_FLASH_STORE -UOTA_SD_STORE -UWEBCONFIG_DISABLED -DOTA_SEEDER_ONLY=1 -DMOTA_TARGET_ID=0 -DCOMPANION_RADIO_FULL=1 -DCOMPANION_FEATURE_TEMP_RADIO=1 -DCOMPANION_FEATURE_OTA_CLI=1 -DENABLE_USB_INTERFACE=1 -DBLE_PIN_CODE=123456 -DMESH_DEBUG=1 -DMESH_PACKET_LOGGING=1" + fi if is_nrf52_companion_radio_full_target "$env_name"; then # CDC 0 starts as an ASCII CLI and automatically hands an incoming framed @@ -5117,6 +5190,7 @@ build_firmware_one_profile() { apply_stm32_companion_size_profile "$env_name" apply_nrf52_size_profile "$env_name" apply_lora_ota_no_external_sensors_profile "$env_name" + apply_rak4631_rak15002_sd_store_profile "$env_name" apply_radio_overrides apply_firmware_profile_overrides declare_build_capability_contract "$env_name" "$env_platform" diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9a90d6a7c..e21a21620 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -5650,7 +5650,47 @@ to charge above it or have USB power restored before it wakes. ### Ethernet (when Ethernet support is compiled in) -Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the `_ethernet` firmware variants (e.g. `RAK_4631_repeater_ethernet`) to enable this feature. +Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) +Ethernet module. Use the `_ethernet` firmware variants (for example, +`RAK_4631_repeater_ethernet`) for the normal Ethernet-only configuration. + +`RAK_4631_companion_radio_full` is a distinct combined profile: it can compile +both physical RAK13800 Ethernet and an optional RAK15002 SD mOTA archive. They +share CS26, so they are mutually exclusive. In that Full profile Ethernet is +off at boot and is deliberately controlled only for the current boot: + +```text +get eth +set eth on +set eth off +get sdcard [free] +ota cache [on|off] +``` + +Before `set eth on`, remove the SD card and finish/cancel any active OTA +capture, transfer, serving, or host-folder work. The command rejects an +inserted card or a busy archive instead of taking the shared bus. Turn +Ethernet off before accessing or inserting the card. An insertion while +Ethernet is live fails closed and still needs `set eth off`; it is not a +supported hot-swap flow. This combined hardware path is not yet qualified. + +The SD-capable Ethernet-room receiver, +`RAK_4631_room_server_ethernet_lora_ota_no_external_sensors`, uses the same +shared CS26 safety rule but has its own cooperative Ethernet control. It starts +off on every reboot and is never persisted. From USB or an already established +Telnet session, use either spelling: + +```text +eth on +# or: set eth on +eth.status +eth off +``` + +`eth on` rejects a fitted card, active/cache/staged OTA work, or a retained NOR +owner. `eth off` waits for a safe release and refuses while OTA work is busy. +Remove the card before enabling Ethernet and do not insert it while Ethernet +is active. --- @@ -5663,8 +5703,12 @@ Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Etherne - `ETH: not connected` when Ethernet is not active **Notes:** -- Available on repeater and room server firmware only. Companion radio ethernet firmware does not expose a CLI. -- The Ethernet interface obtains an IP address via DHCP automatically on boot. +- Available on repeater and room server firmware only. The normal + `_ethernet` Companion image does not expose a CLI; the combined RAK4631 Full + profile above instead exposes `get eth` / `set eth on|off`. +- The normal `_ethernet` variants obtain an IP address via DHCP automatically + on boot. The combined RAK4631 Full profile does not: Ethernet starts off + until `set eth on` succeeds. - A TCP server listens on port 23 (default) for CLI connections. - Connect with any TCP client (e.g. `nc`, PuTTY) to access the same CLI available over serial. diff --git a/docs/companion_radio_full.md b/docs/companion_radio_full.md index 18555b299..4b665e1ff 100644 --- a/docs/companion_radio_full.md +++ b/docs/companion_radio_full.md @@ -30,8 +30,10 @@ requires a reboot to apply its exclusive transport selection; primary ESP-NOW mesh operation is unaffected by the infrastructure-WiFi default. The full Companion is deliberately not a LoRa OTA destination: it has no -firmware staging store, refuses `ota install`, and never advertises its own -firmware as an mOTA image. +installable firmware staging store, refuses `ota install`, and never +advertises its own firmware as an mOTA image. The RAK4631 Full variant can +optionally keep a source-only RAK15002 SD archive of peer `.mota` files; that +does not make the Companion an update destination. Preference changes through the Companion protocol, WebConfig, or USB terminal report a storage error if saving fails, and restore their previous values. @@ -44,13 +46,39 @@ a busy radio rollback remains pending for recovery instead of reporting success. | BLE Binary Companion | Yes | Yes | | USB ASCII terminal | Yes | Yes | | Dedicated USB plaintext logging | No - shares the one USB TTY | Yes | -| Host-backed LoRa mOTA source | WiFi TCP 5001 | Exclusive USB mode or encrypted BLE | +| Host-backed LoRa mOTA source | WiFi TCP 5001 | Exclusive USB mode or encrypted BLE; RAK4631 Full can additionally cache/serve peer images from optional RAK15002 SD | | WiFi Companion/WebConfig | Yes | No - nRF52840 has no WiFi | | Hardware serial Companion | On targets with assigned serial pins | On targets with assigned serial pins | -| Ethernet Companion | On targets with an Ethernet module | On RAK4631 with RAK13800 | +| Ethernet Companion | On targets with an Ethernet module | On RAK4631 with RAK13800; the RAK15002 combination is explicitly one-at-a-time | | LoRa self-update | No | No | | Optional self-update | WiFi on existing dual-app layouts; USB on single-app layouts | Bluetooth DFU with a compatible bootloader; USB always supported | +### RAK4631 Full Companion: optional RAK15002 archive and RAK13800 Ethernet + +`RAK_4631_companion_radio_full` can cache and serve peer mOTA files from an +optional RAK15002 card. This remains a source-only archive: `ota install` and +bootloader installation are unavailable on the Companion, whether or not a +card is inserted. Check the archive with `ota cache [on|off]`; use +`get sdcard [free]` for card space. + +The optional RAK13800 Ethernet module and RAK15002 share CS26. Ethernet is +therefore **off by default** in this combined Full image and is controlled only +for the current boot: + +```text +get eth +set eth on +set eth off +``` + +Remove the SD card and end/cancel active OTA cache, transfer, serve, or host +folder activity before `set eth on`. The command refuses unsafe states. Turn +Ethernet off before inserting or accessing a card; an insertion during +Ethernet use fails closed and still requires `set eth off`. This is not a +supported hot-swap workflow. The combined RAK15002/Ethernet hardware path has +not yet been hardware-qualified; see [nRF52 RAK15002 storage](ota_nrf52_qspi.md#optional-rak15002-storage-in-canonical-rak4631-images) +for the receiver's separate signed-application path and loader prerequisite. + ### Screenless nRF52 spoken button confirmations Full Companion builds for T1000-E, RAK WisMesh Tag, MeshTracker X1, diff --git a/docs/ota_nrf52_qspi.md b/docs/ota_nrf52_qspi.md index 95af4196e..c500d8faa 100644 --- a/docs/ota_nrf52_qspi.md +++ b/docs/ota_nrf52_qspi.md @@ -415,3 +415,124 @@ Source-only means `ota install`, bootloader installation, and `ota dev` apply are disabled even if the installed bootloader supports SD. No RAK15002 SD bootloader handoff is claimed. Update this trial firmware over USB/BLE; normal RAK4631 receiver targets and MeshTower SD self-update are unchanged. + +## Optional RAK15002 storage in canonical RAK4631 images + +This is a separate **STORE** path, not an expansion of the older +`RAK_4631_repeater_rak15002_sd_seeder_only` CACHE-only experiment above. It is +limited to the canonical RAK4631 Full/Reduced LoRa-OTA receiver roles +(repeater, room server, Ethernet room server, and sensor) and to +`RAK_4631_companion_radio_full`. Existing exact-NOR identities, non-OTA roles, +and the CACHE-only seeder retain their previous storage contracts. + +The receiver and Full Companion use the same optional card hardware, but not +the same update authority: + +| Image type | RAK15002 role | Local firmware install from that card | +| --- | --- | --- | +| Canonical RAK4631 receiver | Adaptive application staging, plus the retained mOTA archive/source | Only a trusted, signed **application** package, after bootloader capability checks | +| `RAK_4631_companion_radio_full` | Cache and serve peer `.mota` files | Never; it remains a source-only Companion | +| `RAK_4631_repeater_rak15002_sd_seeder_only` | Historical strict cache/source-only profile | Never | + +The receiver chooses the card only during real OTA/cache work, not at boot or +while merely reporting status. With no card it retains its existing qualified +internal/NOR selection. With a card fitted, the firmware deliberately does +**not** probe NOR as a fallback, even if the card, filesystem, or installed +loader is unsuitable: both devices use CS26, so issuing a NOR probe could +clock the card. RAK15002 card-detect is active-low WisBlock IO6 (Arduino pin +10), not Arduino pin 6; the SD bus is SPIM2 at 4 MHz on 29/3/30 with CS26. + +The new STORE path accepts **FAT16/FAT32 only**. It deliberately uses the +smaller FAT-only SdFat types to fit the RAK4631 flash budget and reduce its +RAM use; an exFAT card is not a supported STORE volume. This restriction does +not change the historical CACHE-only RAK15002 seeder or MeshTower SD profiles, +which retain their existing `SdFs`/`FsFile` (FAT/exFAT) behavior. + +### Future receiver loader prerequisite and update flow + +The matching `wiscore_rak4631_auto` **all-in-one OTAFIX bootloader** is only a +local candidate today: it is not released and has not been hardware-qualified. +Do not provision it as an SD-apply upgrade. Until a released, qualified loader +explicitly advertises the supplemental authenticated RAK15002-SD +*application* capability, the receiver fails SD application apply closed and +continues to use its qualified internal/NOR path when no card is selected. + +Once such a loader is released, it must preserve the deployed internal/NOR +capabilities while adding the separate SD application handoff record. A stock, +older, or otherwise ordinary RAK4631 OTAFIX bootloader must refuse the SD +handoff. Its first migration will need to be performed locally over USB/BLE DFU +or SWD; it must not be bootstrapped from the SD card or with a LoRa bootloader +package. + +The supplemental capability is application-only: an SD card never authorizes +a bootloader update. Existing qualified internal bootloader-update behavior is +separate and still applies only to its exact package and loader identity. The +Full Companion's archive/source feature never installs firmware locally, so it +does not need this SD-apply bootloader capability. + +Only after that future matching loader is released, installed locally, and +qualified may a receiver use the normal explicit application flow: + +```text +ota status +ota ls +ota pull flash +# wait for `ota status` to show the same fetch as ready +ota install +``` + +`ota cache` manages the card-backed archive/source (`ota cache off` stops new +captures; retained files can still seed). `ota storage` and `ota self` report +the selected backend and loader state, but they intentionally do not force a +removable-media probe at startup. After the future loader is present, a +card-backed receiver reports `SD store` and `authenticated SD apply OK`; before +then, a missing supplemental capability, unsigned application, incompatible +media, or a busy store fails before reboot/approval. + +### RAK13800 Ethernet ownership + +The SD-capable Ethernet-room receiver +`RAK_4631_room_server_ethernet_lora_ota_no_external_sensors` is also an +explicit one-at-a-time profile. Its RAK13800 Ethernet starts off on **every** +reboot and is not a saved setting. With the card removed and no active/cache, +staged, serving, or NOR work, enable it from local USB or an existing Telnet +session with either command spelling: + +```text +eth on +# or: set eth on +eth.status +eth off +``` + +`eth on`/`set eth on` refuses a fitted card, retained NOR owner, or busy OTA +state. `eth off`/`set eth off` is cooperative and likewise refuses to release +the bus while OTA work is active. Do not use these commands as a way to switch +media during a transfer. + +#### Full Companion + +The Full Companion is deliberately different: its optional RAK15002 holds +peer archives for caching and serving, never its own install image. It may +also compile RAK13800 Ethernet, but Ethernet and the card electrically share +CS26 and cannot operate together. In this combined profile physical Ethernet +starts **off** and `set eth on` is only a this-boot choice. + +To use Ethernet, stop/cancel any active OTA capture or serve work, detach any +host folder source, **physically remove the SD card**, then run: + +```text +get eth +set eth on +``` + +The command refuses unsafe ownership states rather than forcing the bus. To +return to the card, run `set eth off`, wait for it to report off, then insert +the card and use `get sdcard [free]` or `ota cache [on|off]`. If a card is +inserted while Ethernet is active, Ethernet fails closed; use `set eth off` +before accessing the card. This is not a supported hot-swap procedure. + +The combined RAK15002 receiver/Companion/RAK13800 path has host-side checks, +but **has not yet received hardware qualification**. Do not treat these rules +as proof that a particular card, Ethernet module, loader migration, or live +card insertion has been field-tested. diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 81aa801cb..0700d92da 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -53,6 +53,9 @@ #endif #if defined(ENABLE_OTA) #include +#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800) +bool handleRak4631PeripheralCommand(const char* command, char* reply, size_t capacity); +#endif #endif #if defined(ENABLE_OTA) #include @@ -3787,6 +3790,9 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply, size_t reply_size) { if (!command || !reply || reply_size == 0) return false; while (*command == ' ') command++; +#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800) + if (handleRak4631PeripheralCommand(command, reply, reply_size)) return true; +#endif #if defined(MESH_HIL_TX_FREQ_MHZ) if (mesh::handleHilTxFrequencyCommand(command, reply, reply_size)) return true; #endif @@ -10234,6 +10240,10 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt, terminalOutput().print(" get ethernet / get ethernet.radio\r\n" " set ethernet.radio ,,,[,]\r\n" " set ethernet on|off (saved; trusted-LAN KISS TCP 5003, radio2 only)\r\n"); +#endif +#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800) + terminalOutput().print(" get eth / set eth on|off (physical RAK13800; default off, this boot)\r\n" + " get sdcard [free] / ota cache [on|off] (RAK15002; mutually exclusive with Ethernet)\r\n"); #endif terminalOutput().print(" get pwrmgt.bootreason\r\n"); #if COMPANION_FEATURE_MEMORY_DIAGNOSTICS diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 801f67f34..b828df8d5 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -653,6 +653,53 @@ static void cancelCompanionEthernetSession(void*) { the_mesh.cancelSerialOperationsForRoute(ðernet_interface); interface_manager.forgetReplyRouteForDisconnected(ðernet_interface); } +#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800) +static bool prepareCompanionRakEthernet(void*) { + auto* context = mesh::ota::ota_context_if_active(); + return !context || context->prepareRakSdForEthernet(); +} +static bool canReleaseCompanionRakEthernet(void*) { + auto* context = mesh::ota::ota_context_if_active(); + return !context || context->canReleaseRakSdForEthernet(); +} +static bool releasedCompanionRakEthernet(void*) { + auto* context = mesh::ota::ota_context_if_active(); + return !context || context->releasedRakSdEthernet(); +} + +bool handleRak4631PeripheralCommand(const char* command, char* reply, size_t capacity) { + const bool get = strcmp(command, "get eth") == 0; + const char* value = strncmp(command, "set eth ", 8) == 0 ? command + 8 : nullptr; + if (get || value) { + if (value && strcmp(value, "on") && strcmp(value, "off")) { + snprintf(reply, capacity, "Error: use set eth on|off"); + } else if (value && !(strcmp(value, "on") == 0 + ? ethernet_interface.tryEnable() : ethernet_interface.tryDisable())) { + snprintf(reply, capacity, "Error: %s", ethernet_interface.last_error()); + } else { + snprintf(reply, capacity, "%s eth=%s; RAK13800 (this boot)", get ? ">" : "OK -", + ethernet_interface.hardwareActive() ? "on" : "off"); + } + return true; + } + if (strcmp(command, "get sdcard") == 0 || strcmp(command, "get sdcard free") == 0) { + if (ethernet_interface.hardwareActive()) { + snprintf(reply, capacity, "Error: turn Ethernet off before accessing SD"); + } else if (mesh::ota::ota_acquire_context(reply, capacity)) { + uint64_t used = 0, free = 0; + auto& card = mesh::ota::ota_ctx().sdStagingStore(); + if (card.getSpace(board, used, free)) { + snprintf(reply, capacity, "> SD used=%llu free=%llu bytes; ota cache for archive status", + (unsigned long long)used, (unsigned long long)free); + } else { + snprintf(reply, capacity, "Error: %s", card.last_error()); + } + } + return true; + } + return false; +} +#endif #endif #if defined(ENABLE_USB_INTERFACE) @@ -3950,6 +3997,10 @@ void setup() { // add ethernet interface #if defined(ETHERNET_ENABLED) ethernet_interface.setSessionChangedCallback(cancelCompanionEthernetSession, nullptr); +#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800) + ethernet_interface.setSpiOwnershipHooks(prepareCompanionRakEthernet, + canReleaseCompanionRakEthernet, releasedCompanionRakEthernet, nullptr); +#endif ethernet_interface.begin(); interface_manager.addInterface(InterfaceType::Ethernet, ðernet_interface); #endif diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 9b8742c10..104d6e080 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -86,17 +86,40 @@ static bool prepareCombinedEthernet(void*, char* error, size_t capacity) { snprintf(error, capacity, "Error: OTA storage is not ready"); return false; } +#if defined(OTA_RAK15002_SD_STORE) + if (mesh::ota::ota_rak15002_sd_card_present()) { + snprintf(error, capacity, "Error: RAK15002 SD card fitted; Ethernet shares its IO slot"); + return false; + } + if (!context->prepareRakSdForEthernet() || + !mesh::ota::rak4631_try_acquire_spi(mesh::ota::RakSharedSpiOwner::Ethernet)) { + snprintf(error, capacity, "Error: Ethernet bus is locked by OTA activity or storage"); + return false; + } + return true; +#else return context->prepareRakEthernet(error, capacity); +#endif } static bool canReleaseCombinedEthernet(void*) { auto* context = mesh::ota::ota_context_if_active(); +#if defined(OTA_RAK15002_SD_STORE) + return context && context->canReleaseRakSdForEthernet(); +#else return context && context->canReleaseRakEthernet(); +#endif } static bool releaseCombinedEthernet(void*) { auto* context = mesh::ota::ota_context_if_active(); +#if defined(OTA_RAK15002_SD_STORE) + if (!context || !context->releasedRakSdEthernet()) return false; + mesh::ota::rak4631_release_spi(mesh::ota::RakSharedSpiOwner::Ethernet); + return true; +#else return context && context->releaseRakEthernet(); +#endif } #endif @@ -372,6 +395,9 @@ void setup() { hooks.prepare = prepareCombinedEthernet; hooks.can_release = canReleaseCombinedEthernet; hooks.released = releaseCombinedEthernet; +#if defined(OTA_RAK15002_SD_STORE) + hooks.storage_conflict = [](void*) { return mesh::ota::ota_rak15002_sd_card_present(); }; +#endif ethernet_configure(hooks); // First prototype defaults OFF on every restart. #endif ethernet_start_task(); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 109be6084..c9fa42daa 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -41,6 +41,42 @@ SimpleMeshTables tables; MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables); #include "../InfrastructureWireless.h" +#if defined(ETHERNET_ENABLED) && defined(OTA_RAK15002_SD_STORE) +#include +// The RAK15002 and RAK13800 occupy the same IO-slot chip select. Ethernet is +// opt-in, and only the mesh thread may release cached sources/staging owners +// and reserve the bus before its asynchronous DHCP worker starts. +static bool prepareSdEthernet(void*, char* error, size_t capacity) { + auto* context = mesh::ota::ota_context_if_active(); + if (!context) { + snprintf(error, capacity, "Error: OTA storage is not ready"); + return false; + } + if (mesh::ota::ota_rak15002_sd_card_present()) { + snprintf(error, capacity, "Error: RAK15002 SD card fitted; Ethernet shares its IO slot"); + return false; + } + if (!context->prepareRakSdForEthernet() || + !mesh::ota::rak4631_try_acquire_spi(mesh::ota::RakSharedSpiOwner::Ethernet)) { + snprintf(error, capacity, "Error: Ethernet bus is locked by OTA activity or storage"); + return false; + } + return true; +} + +static bool canReleaseSdEthernet(void*) { + auto* context = mesh::ota::ota_context_if_active(); + return context && context->canReleaseRakSdForEthernet(); +} + +static bool releaseSdEthernet(void*) { + auto* context = mesh::ota::ota_context_if_active(); + if (!context || !context->releasedRakSdEthernet()) return false; + mesh::ota::rak4631_release_spi(mesh::ota::RakSharedSpiOwner::Ethernet); + return true; +} +#endif + void halt() { while (1) ; } @@ -226,6 +262,14 @@ void setup() { #endif #ifdef ETHERNET_ENABLED +#if defined(OTA_RAK15002_SD_STORE) + EthernetCliHooks hooks; + hooks.prepare = prepareSdEthernet; + hooks.can_release = canReleaseSdEthernet; + hooks.released = releaseSdEthernet; + hooks.storage_conflict = [](void*) { return mesh::ota::ota_rak15002_sd_card_present(); }; + ethernet_configure(hooks); // Shared IO slot defaults OFF on every restart. +#endif ethernet_start_task(); #endif @@ -316,7 +360,11 @@ void loop() { #ifdef ETHERNET_ENABLED ethernet_loop_maintain(); +#if defined(OTA_RAK15002_SD_STORE) + if (ethernet_take_session_reset() || !ethernet_get_enabled()) { +#else if (ethernet_take_session_reset() || !ethernet_client.connected()) { +#endif the_mesh.cancelLocalOutput(ethernet_client); ethernet_command[0] = 0; } diff --git a/platformio.ini b/platformio.ini index e82aa3ef5..9ad05fe8c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -298,6 +298,34 @@ build_flags = -D OTA_QSPI_SCK_FREQUENCY=NRF_QSPI_FREQ_32MDIV4 -D OTA_QSPI_EXPECTED_SIZE=2097152UL +; RAK15002 is an optional SD card in the WisBlock IO slot. The RAK4631 +; runtime owns the shared slot at runtime: an installed card selects SD and a +; card-free board can retain its internal/NOR OTA path. Keep these physical +; values in one recipe so receiver and Full-Companion variants cannot drift. +; This is deliberately STORE rather than the old CACHE-only experimental +; profile; the latter remains a strict source-only, no-Ethernet recipe. +[nrf52_rak15002_sd_store] +build_flags = + -D OTA_RAK15002_SD_STORE=1 + -D OTA_SD_STORE=1 + -D OTA_SD_CS_PIN=26 + -D OTA_SD_SCK_MHZ=4 + -D OTA_SD_CACHE_MAX=16 + -D OTA_MAX_SERVE=24 +lib_deps = + https://github.com/greiman/SdFat-beta.git#a249cfa121e25a96ee14d8813abf01755f1f6a25 + +; Canonical RAK4631 receiver OTA profile: select internal flash, exact NOR, +; or an inserted RAK15002 card at runtime. Individual exact-NOR compatibility +; targets remain separate because their identities and bootloader contracts +; are already deployed. +[nrf52_rak4631_unified_ota] +build_flags = + ${nrf52_wisblock_auto_ota.build_flags} + ${nrf52_rak15002_sd_store.build_flags} +lib_deps = + ${nrf52_rak15002_sd_store.lib_deps} + ; Compatibility alias retained for existing nRF52840 board definitions. [rak4631_hw] extends = nrf52_lora_ota diff --git a/scripts/check_firmware_ram.py b/scripts/check_firmware_ram.py index 5ec50d26c..656f47bc2 100644 --- a/scripts/check_firmware_ram.py +++ b/scripts/check_firmware_ram.py @@ -158,6 +158,8 @@ def requirements(platform, defines, target): parts["display_dma_and_bus"] = 16384 mqtt = None combined_rak_ethernet = "RAK4631_COMBINED_ETHERNET" in defines + cooperative_rak_ethernet = combined_rak_ethernet or ( + "OTA_RAK15002_SD_STORE" in defines and "ETHERNET_ENABLED" in defines and not companion) if combined_rak_ethernet and (platform != "NRF52_PLATFORM" or not {"RAK_4631", "ETHERNET_ENABLED", "OTA_RAK_AUTO_STORE"}.issubset(defines)): raise ValueError("combined RAK Ethernet budget requires RAK4631 adaptive nRF52 storage") @@ -172,9 +174,19 @@ def requirements(platform, defines, target): if platform == "NRF52_PLATFORM": parts["loop_and_callback_stacks"] = integer(defines, "MESH_NRF52_LOOP_STACK_WORDS", 2048) * 4 + 3072 parts["core_usb_filesystems_sensors"] = 12288 + if "OTA_RAK15002_SD_STORE" in defines: + # New optional RAK SD allocates one SdFs volume and separate + # staging/archive FsFile handles lazily; production bounds the + # complete objects plus allocation metadata to this allowance. + parts["sd_fat_volume_and_files"] = 2048 + if full and integer(defines, "OTA_TARGET_NAME_FRONT_CODED", 0): + # RAK Full uses larger independent Zopfli chunks for its + # complete name index, with a rare, freed-on-return heap + # lookup buffer instead of increasing the loop stack. + parts["ota_target_name_lookup"] = 4096 + 16 if "BLE_PIN_CODE" in defines: parts["bluetooth_worker_stacks"] = 5920 - if combined_rak_ethernet and "WITH_RS232_BRIDGE" in defines: + if cooperative_rak_ethernet and "WITH_RS232_BRIDGE" in defines: # The bridge owns a packet/ACK duplicate table, RX/TX frames and # heartbeat state. The source assertion bounds the complete object # to 2816 bytes; retain room for allocation metadata and the UART TX semaphore. @@ -278,7 +290,7 @@ def requirements(platform, defines, target): # running nRF52840 app is <1 MiB: <=512 2-KiB leaves. Compression needs # one block; raw diagnostic serving retains a leaf-sized output. parts["ota_self_source_scratch"] = 2 * 2048 + 2 * 16 - if combined_rak_ethernet: + if cooperative_rak_ethernet: words = integer(defines, "RAK4631_ETHERNET_TASK_STACK_WORDS", 1024) if words < 1024: raise ValueError("combined RAK Ethernet task requires at least 1024 stack words") @@ -333,7 +345,7 @@ def requirements(platform, defines, target): largest = max(largest, 8192 + parts["expanded_message_previews"]) for name in ("client_table", "flood_filter_table", "room_flood_rule_table", "neighbor_history", "ota_context", "screen_objects_and_history", "ota_encoder_workspace", - "ota_generic_source_buffers", "ethernet_or_external_ota_workspaces"): + "ota_generic_source_buffers", "ethernet_or_external_ota_workspaces", "ota_target_name_lookup"): largest = max(largest, parts.get(name, 0)) if "room_browser_mailbox" in parts: largest = max(largest, 8192, parts["room_browser_mailbox"]) diff --git a/src/helpers/StorageLayout.cpp b/src/helpers/StorageLayout.cpp index 9906dc9f3..c6fff525f 100644 --- a/src/helpers/StorageLayout.cpp +++ b/src/helpers/StorageLayout.cpp @@ -76,7 +76,26 @@ void formatStorageLayout(MainBoard& board, char* reply, size_t reply_size) { (unsigned long)internal_fs_start, (unsigned long)(internal_fs_size / 1024UL)); -#if defined(ENABLE_OTA) && defined(OTA_RAK_AUTO_STORE) +#if defined(ENABLE_OTA) && defined(OTA_RAK15002_SD_STORE) + (void)board; + // Optional SD discovery can be slow and takes ownership of the IO-slot + // bus. A layout/status query must not mount it or allocate an OTA context. + const auto* context = mesh::ota::ota_context_if_active(); + if (!context) { + appendStorageLayout(reply, reply_size, "; ext:sd=optional ota=not-prepared"); + } else { +#if defined(OTA_SEEDER_ONLY) + appendStorageLayout(reply, reply_size, "; ext:sd=optional owner=ota-archive source-only"); +#else + const auto& store = context->fetch_store; + appendStorageLayout(reply, reply_size, "; ota=%s medium=%s staged=%lu", + store.selectionReason(), + store.usesSd() ? "sd" : store.usesQspi() ? "qspi" : + store.usesInternal() ? "internal" : "not-prepared", + (unsigned long)store.staged_size()); +#endif + } +#elif defined(ENABLE_OTA) && defined(OTA_RAK_AUTO_STORE) (void)board; mesh::ota::OtaStoreAdaptiveNrf52& store = mesh::ota::ota_ctx().fetch_store; if (store.usesExternal()) { diff --git a/src/helpers/UserGpioPinPolicy.h b/src/helpers/UserGpioPinPolicy.h index e30d62d05..a1bb59f72 100644 --- a/src/helpers/UserGpioPinPolicy.h +++ b/src/helpers/UserGpioPinPolicy.h @@ -21,6 +21,13 @@ namespace UserGpioPinPolicy { inline bool isFirmwareReserved(uint8_t pin) { +#if defined(OTA_RAK15002_SD_STORE) + // The exact RAK15002 recipe claims these WisBlock nets even when no card + // is fitted: user GPIO must not defeat card detection or a later SD/NOR/ + // Ethernet handoff. WB_IO6 is Arduino pin 10, not GPIO number 6. + if (pin == PIN_SPI_SCK || pin == WB_IO6 || pin == 26 + || pin == PIN_SPI_MISO || pin == PIN_SPI_MOSI) return true; +#endif #if defined(STATION_G3_ESP32) // A module may be present in the inactive slot too. Never offer its pins // for user GPIO, even when the current boot selected the other slot. diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp index f1b975af6..c97099303 100644 --- a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp @@ -4,6 +4,9 @@ #include "../../nrf52/EthernetCliTransmit.h" #include #include +#if defined(OTA_RAK15002_SD_STORE) + #include "../../ota/OtaSdSpiBusNrf52.h" +#endif #define PIN_SPI1_MISO (29) // (0 + 29) #define PIN_SPI1_MOSI (30) // (0 + 30) @@ -16,6 +19,64 @@ SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI #define PIN_ETHERNET_SS 26 bool RAK13800EthernetInterface::begin() { +#if defined(OTA_RAK15002_SD_STORE) + // Rare shared-CS Ethernet is explicitly enabled after the SD/NOR context + // has supplied its pin/transfer ownership hooks. Boot remains USB/BLE-only. + return mesh::nrf52::registerTerminalEthernetShutdown(terminalShutdown, this); +#else + return beginHardware(); +#endif +} + +#if defined(OTA_RAK15002_SD_STORE) +bool RAK13800EthernetInterface::tryEnable() { + if (_terminalStopping) { _spiError = "terminal shutdown"; return false; } + if (_spiStarted) { SerialEthernetInterface::enable(); return true; } + if (_ownsSharedSpi) { _spiError = "release stopped Ethernet with set eth off first"; return false; } + if (!_prepareSpi || !_canReleaseSpi || !_releasedSpi) { + _spiError = "OTA ownership hooks unavailable"; return false; + } + if (mesh::ota::ota_rak15002_sd_card_present()) { + _spiError = "remove SD card: Ethernet and SD share CS26"; return false; + } + if (!_prepareSpi(_spiContext)) { _spiError = "OTA storage busy"; return false; } + if (!mesh::ota::rak4631_try_acquire_spi(mesh::ota::RakSharedSpiOwner::Ethernet)) { + (void)_releasedSpi(_spiContext); + _spiError = "WisBlock SPI busy"; return false; + } + _ownsSharedSpi = true; + if (!beginHardware()) { + (void)shutdownForPowerOff(0); + _terminalStopping = false; + (void)_releasedSpi(_spiContext); + _spiError = "Ethernet startup failed"; return false; + } + SerialEthernetInterface::enable(); + _spiError = "on"; + return true; +} + +bool RAK13800EthernetInterface::tryDisable() { + if (_terminalStopping) { _spiError = "terminal shutdown"; return false; } + if (!_ownsSharedSpi) { + SerialEthernetInterface::disable(); + _spiError = "off"; + return true; + } + if (_ownsSharedSpi && (!_canReleaseSpi || !_canReleaseSpi(_spiContext))) { + _spiError = "OTA storage busy"; return false; + } + (void)shutdownForPowerOff(0); + _terminalStopping = false; + if (_releasedSpi && !_releasedSpi(_spiContext)) { + _spiError = "OTA storage release pending"; return false; + } + _spiError = "off"; + return true; +} + +#endif +bool RAK13800EthernetInterface::beginHardware() { if (_terminalStopping) return false; if (!mesh::nrf52::registerTerminalEthernetShutdown(terminalShutdown, this)) return false; // Preserve existing startup/link timing. PoE supplies separate 5V through @@ -83,29 +144,53 @@ bool RAK13800EthernetInterface::shutdownForPowerOff(uint32_t timeout_ms) { // disable() cancels framed/session producers, but disconnectClient() must // not enter the vendor socket-stop/ACK wait during terminal shutdown. SerialEthernetInterface::disable(); - if (!_spiStarted) return true; - for (uint8_t socket = 0; socket < 4; ++socket) { - (void)mesh::nrf52::EthernetCliTransmit::close(socket); + if (_spiStarted) { +#if defined(OTA_RAK15002_SD_STORE) + // A newly inserted card also selects on CS26. Do not clock Ethernet + // socket commands into it during recovery or terminal shutdown. + if (!mesh::ota::ota_rak15002_sd_card_present()) +#endif + for (uint8_t socket = 0; socket < 4; ++socket) { + (void)mesh::nrf52::EthernetCliTransmit::close(socket); + } + client = EthernetClient(); + ETHERNET_SPI_PORT.end(); + _spiStarted = false; + mesh::nrf52::releaseRakEthernetBusPads(); } - client = EthernetClient(); - ETHERNET_SPI_PORT.end(); - _spiStarted = false; - mesh::nrf52::releaseRakEthernetBusPads(); +#if defined(OTA_RAK15002_SD_STORE) + if (_ownsSharedSpi) { + mesh::ota::rak4631_release_spi(mesh::ota::RakSharedSpiOwner::Ethernet); + _ownsSharedSpi = false; + } +#endif return true; } int RAK13800EthernetInterface::available() { if (!_spiStarted || _terminalStopping) return 0; +#if defined(OTA_RAK15002_SD_STORE) + if (mesh::ota::ota_rak15002_sd_card_present() + || !mesh::ota::rak4631_ethernet_owns_spi()) return 0; +#endif return client.available(); } int RAK13800EthernetInterface::read() { if (!_spiStarted || _terminalStopping) return -1; +#if defined(OTA_RAK15002_SD_STORE) + if (mesh::ota::ota_rak15002_sd_card_present() + || !mesh::ota::rak4631_ethernet_owns_spi()) return -1; +#endif return client.read(); } size_t RAK13800EthernetInterface::write(const uint8_t *buf, size_t size) { if (!_spiStarted || _terminalStopping) return 0; +#if defined(OTA_RAK15002_SD_STORE) + if (mesh::ota::ota_rak15002_sd_card_present() + || !mesh::ota::rak4631_ethernet_owns_spi()) return 0; +#endif return client.write(buf, size); } @@ -115,11 +200,31 @@ bool RAK13800EthernetInterface::isConnected() const { void RAK13800EthernetInterface::disconnectClient() { _isConnected = false; +#if defined(OTA_RAK15002_SD_STORE) + if (mesh::ota::ota_rak15002_sd_card_present() + || !mesh::ota::rak4631_ethernet_owns_spi()) return; +#endif if (_spiStarted && !_terminalStopping) client.stop(); } void RAK13800EthernetInterface::loop() { if (!_spiStarted || _terminalStopping) return; +#if defined(OTA_RAK15002_SD_STORE) + if (mesh::ota::ota_rak15002_sd_card_present()) { + // Fail closed on insertion while Ethernet owns the bus. Do not send a + // socket-close command on shared CS26 or switch a live internal OTA store. + _terminalStopping = true; + SerialEthernetInterface::disable(); + _terminalStopping = false; + digitalWrite(PIN_ETHERNET_SS, HIGH); + ETHERNET_SPI_PORT.end(); + _spiStarted = false; + mesh::nrf52::releaseRakEthernetBusPads(); + _spiError = "SD inserted; set eth off before SD access"; + return; // Retain Ethernet's claim until the context permits release. + } + if (!mesh::ota::rak4631_ethernet_owns_spi()) return; +#endif Ethernet.maintain(); if (!isEnabled()) return; if (_isConnected && !client.connected()) { diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h index a66fed7d0..4eaeb129d 100644 --- a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h @@ -11,6 +11,15 @@ class RAK13800EthernetInterface : public SerialEthernetInterface { bool _terminalStopping = false; EthernetServer server; EthernetClient client; + bool beginHardware(); +#if defined(OTA_RAK15002_SD_STORE) + bool (*_prepareSpi)(void*) = nullptr; + bool (*_canReleaseSpi)(void*) = nullptr; + bool (*_releasedSpi)(void*) = nullptr; + void* _spiContext = nullptr; + bool _ownsSharedSpi = false; + const char* _spiError = "off"; +#endif protected: void disconnectClient() override; @@ -24,6 +33,21 @@ class RAK13800EthernetInterface : public SerialEthernetInterface { bool begin(); void loop() override; +#if defined(OTA_RAK15002_SD_STORE) + void setSpiOwnershipHooks(bool (*prepare)(void*), bool (*can_release)(void*), + bool (*released)(void*), void* context) { + _prepareSpi = prepare; _canReleaseSpi = can_release; + _releasedSpi = released; _spiContext = context; + } + bool tryEnable(); + bool tryDisable(); + bool hardwareActive() const { return _spiStarted; } + const char* last_error() const { return _spiError; } + // The transport manager enables every registered interface at boot. Do + // not turn that logical operation into a physical shared-bus acquisition. + void enable() override { if (_spiStarted) SerialEthernetInterface::enable(); } + void disable() override { (void)tryDisable(); } +#endif // BaseSerialInterface methods bool isConnected() const override; diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h index de82df631..d523495d3 100644 --- a/src/helpers/nrf52/EthernetCLI.h +++ b/src/helpers/nrf52/EthernetCLI.h @@ -39,19 +39,28 @@ static SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SP #define ETHERNET_RETRY_INTERVAL_MS 30000 +// New optional-SD infrastructure recipes need the same cooperative, OFF by +// default bus handoff as the combined prototype. Never start legacy DHCP on +// the shared IO-slot CS merely because Ethernet was compiled into the image. +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) + #define MESH_RAK_ETHERNET_CLI_RUNTIME 1 +#else + #define MESH_RAK_ETHERNET_CLI_RUNTIME 0 +#endif + static EthernetServer ethernet_server(ETHERNET_TCP_PORT); -#if !defined(RAK4631_COMBINED_ETHERNET) +#if !MESH_RAK_ETHERNET_CLI_RUNTIME static EthernetClient ethernet_client; #endif static std::atomic ethernet_running{false}; -#if defined(RAK4631_COMBINED_ETHERNET) +#if MESH_RAK_ETHERNET_CLI_RUNTIME static std::atomic ethernet_session_reset{false}; static bool ethernet_line_discarding = false; // Main-thread framing state. #else static bool ethernet_session_reset = false; #endif static bool ethernet_take_session_reset() { -#if defined(RAK4631_COMBINED_ETHERNET) +#if MESH_RAK_ETHERNET_CLI_RUNTIME const bool changed = ethernet_session_reset.exchange(false); if (changed) ethernet_line_discarding = false; return changed; @@ -62,7 +71,7 @@ static bool ethernet_take_session_reset() { #endif } -#if defined(RAK4631_COMBINED_ETHERNET) +#if MESH_RAK_ETHERNET_CLI_RUNTIME static void ethernet_check_client(); #include "EthernetCliRuntime.h" #else @@ -174,7 +183,7 @@ static void ethernet_start_task() { // Format ethernet status into reply buffer. Returns true if command was handled. static bool ethernet_handle_command(const char* command, char* reply) { -#if defined(RAK4631_COMBINED_ETHERNET) +#if MESH_RAK_ETHERNET_CLI_RUNTIME if (strcmp(command, "eth on") == 0 || strcmp(command, "set eth on") == 0) { (void)ethernet_request_start(reply, true); return true; @@ -193,7 +202,7 @@ static bool ethernet_handle_command(const char* command, char* reply) { } #endif if (strcmp(command, "eth.status") == 0) { -#if defined(RAK4631_COMBINED_ETHERNET) +#if MESH_RAK_ETHERNET_CLI_RUNTIME const EthernetCliState state = ethernet_state.load(); if (state != EthernetCliState::Online) { const char* status = state == EthernetCliState::Off ? "off" : @@ -228,7 +237,8 @@ static bool ethernet_handle_command(const char* command, char* reply) { // available() also returns existing connected sockets that have data, which // would force us to disambiguate every inbound packet from a real new client. static void ethernet_check_client() { -#if defined(RAK4631_COMBINED_ETHERNET) +#if MESH_RAK_ETHERNET_CLI_RUNTIME + if (ethernet_storage_conflict()) return; // Combined accept/relisten can call vendor controller commands; run only // in the background worker while the mesh-facing Stream is gated. auto newClient = ethernet_server.accept(); @@ -272,7 +282,10 @@ static void ethernet_check_client() { // Call from loop() to maintain DHCP and check for new clients static void ethernet_loop_maintain() { -#if defined(RAK4631_COMBINED_ETHERNET) +#if MESH_RAK_ETHERNET_CLI_RUNTIME +#if defined(OTA_RAK15002_SD_STORE) + if (ethernet_enabled.load() && ethernet_storage_conflict()) return; +#endif if (ethernet_state.load() == EthernetCliState::Stopping) { if (ethernet_worker_active.load() && !ethernet_worker_stopped.load()) { EthernetSpiAccess expected = EthernetSpiAccess::Idle; @@ -309,7 +322,7 @@ static void ethernet_loop_maintain() { // Returns true when a complete line is ready to process (command is null-terminated). // The caller should process the command and then reset ethernet_command[0] = 0. static bool ethernet_read_line(char* ethernet_command, size_t buf_size) { -#if defined(RAK4631_COMBINED_ETHERNET) +#if MESH_RAK_ETHERNET_CLI_RUNTIME if (buf_size < 2) return false; EthernetClientAccess access; if (!access || !ethernet_client_attached.load()) return false; @@ -389,7 +402,7 @@ static bool ethernet_read_line(char* ethernet_command, size_t buf_size) { // Send a reply to the Ethernet client static void ethernet_send_reply(const char* reply) { if (reply[0]) { -#if defined(RAK4631_COMBINED_ETHERNET) +#if MESH_RAK_ETHERNET_CLI_RUNTIME // One RAM-only enqueue keeps a synchronous command's complete reply even // if the worker begins renewal between command dispatch and this call. char frame[170]; diff --git a/src/helpers/nrf52/EthernetCliRuntime.h b/src/helpers/nrf52/EthernetCliRuntime.h index aa939f790..a4a91c122 100644 --- a/src/helpers/nrf52/EthernetCliRuntime.h +++ b/src/helpers/nrf52/EthernetCliRuntime.h @@ -3,13 +3,17 @@ #include #include "EthernetCliTransmit.h" -// Included by EthernetCLI.h only for the opt-in combined RAK4631 prototype. -// Legacy Ethernet profiles retain their established implementation. +// Included by EthernetCLI.h for combined or optional-SD RAK4631 recipes. +// Both require explicit startup and a completed storage-owner handoff before +// any worker may touch the IO-slot pins. Legacy-only profiles are unchanged. struct EthernetCliHooks { bool (*prepare)(void*, char*, size_t) = nullptr; bool (*can_release)(void*) = nullptr; bool (*released)(void*) = nullptr; bool (*save)(void*, bool) = nullptr; +#if defined(OTA_RAK15002_SD_STORE) + bool (*storage_conflict)(void*) = nullptr; // GPIO-only, safe from either thread. +#endif void* context = nullptr; }; @@ -41,6 +45,9 @@ static uint32_t ethernet_line_generation = 0; // Main-thread partial input owner static std::atomic ethernet_command_check_requested{false}; static std::atomic ethernet_command_check_started{0}; static bool ethernet_controller_faulted = false; +#if defined(OTA_RAK15002_SD_STORE) +static std::atomic ethernet_storage_faulted{false}; +#endif static mesh::nrf52::EthernetCliTransmit ethernet_transmit; #ifndef ETHERNET_CLI_TX_BUFFER_BYTES @@ -71,6 +78,25 @@ static void ethernet_notify_worker() { taskEXIT_CRITICAL(); } +static bool ethernet_storage_conflict() { +#if defined(OTA_RAK15002_SD_STORE) + if (ethernet_storage_faulted.load()) return true; + if (ethernet_bus_reserved && (!ethernet_hooks.storage_conflict || + ethernet_hooks.storage_conflict(ethernet_hooks.context))) { + // A fitted card shares CS26. Gate new socket/controller transactions + // immediately, but leave the worker to finish any already-entered vendor + // call before ending SPI. Retain ownership until an explicit safe OFF. + ethernet_storage_faulted.store(true); + ethernet_running.store(false); + ethernet_state.store(EthernetCliState::Faulted); + ethernet_session_reset.store(true); + ethernet_notify_worker(); + return true; + } +#endif + return false; +} + static void ethernet_queue_clear() { taskENTER_CRITICAL(); ethernet_tx_head = 0; @@ -116,10 +142,10 @@ class EthernetClientAccess { bool acquired = false; public: EthernetClientAccess() { - if (!ethernet_running.load()) return; + if (ethernet_storage_conflict() || !ethernet_running.load()) return; EthernetSpiAccess expected = EthernetSpiAccess::Idle; acquired = ethernet_spi_access.compare_exchange_strong(expected, EthernetSpiAccess::Client); - if (acquired && !ethernet_running.load()) { + if (acquired && (ethernet_storage_conflict() || !ethernet_running.load())) { ethernet_spi_access.store(EthernetSpiAccess::Idle); acquired = false; } @@ -210,7 +236,7 @@ static void ethernet_stop_hardware() { // The W5100S has four sockets; there is no EthernetServer::end() in the // library. Stop listener, accepted-client and DHCP sockets before SPI.end. // Never toggle the PHY reset or shared WisBlock supply during shutdown. - if (ethernet_spi_started && ethernet_hardware_found) { + if (!ethernet_storage_conflict() && ethernet_spi_started && ethernet_hardware_found) { for (uint8_t index = 0; index < 4; ++index) { (void)mesh::nrf52::EthernetCliTransmit::close(index); } @@ -319,6 +345,7 @@ static bool ethernet_publish_online() { } static void ethernet_cache_ip() { + if (ethernet_storage_conflict()) return; const IPAddress ip = Ethernet.localIP(); ethernet_cached_ip.store(static_cast(ip[0]) | (static_cast(ip[1]) << 8) | @@ -334,6 +361,7 @@ static bool ethernet_has_tx_work() { } static bool ethernet_controller_commands_idle() { + if (ethernet_storage_conflict()) return false; SPIClass* spi = W5100.getSPI(); if (!spi) return false; spi->beginTransaction(SPI_ETHERNET_SETTINGS); @@ -359,6 +387,7 @@ static void ethernet_poll_command_check() { // Worker-only, while SPI access is Worker. No library socketSend/execCmdSn // calls: stalled TCP acknowledgement must never hold the mesh loop. static void ethernet_pump_tx() { + if (ethernet_storage_conflict()) return; ethernet_transmit.poll(millis()); if (ethernet_transmit.failed()) { if (ethernet_raw_client) { @@ -395,9 +424,10 @@ static void ethernet_pump_tx() { } static void ethernet_drain_before_stop() { - if (!ethernet_spi_started || !ethernet_hardware_found) return; + if (ethernet_storage_conflict() || !ethernet_spi_started || !ethernet_hardware_found) return; const uint32_t started = millis(); while (ethernet_has_tx_work() && static_cast(millis() - started) < 250) { + if (ethernet_storage_conflict()) return; ethernet_pump_tx(); if (!ethernet_has_tx_work()) break; (void)ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10)); @@ -409,7 +439,7 @@ static void ethernet_drain_before_stop() { // OFF interrupts retry waits, then lets an in-progress library call finish; // never delete another task in the middle of its SPI transaction. static void ethernet_task(void*) { - if (ethernet_enabled.load()) { + if (ethernet_enabled.load() && !ethernet_storage_conflict()) { pinMode(PIN_ETHERNET_RESET, OUTPUT); digitalWrite(PIN_ETHERNET_RESET, HIGH); ETHERNET_SPI_PORT.begin(); @@ -418,7 +448,9 @@ static void ethernet_task(void*) { uint8_t mac[6]; generateEthernetMac(mac); while (ethernet_enabled.load()) { + if (ethernet_storage_conflict()) break; const bool connected = Ethernet.begin(mac, 10000, 2000) != 0; + if (ethernet_storage_conflict()) break; ethernet_hardware_found = Ethernet.hardwareStatus() != EthernetNoHardware; if (!ethernet_enabled.load()) break; if (!ethernet_hardware_found) { @@ -427,6 +459,7 @@ static void ethernet_task(void*) { return; } if (connected) { + if (ethernet_storage_conflict()) break; ethernet_server.begin(); ethernet_cache_ip(); ethernet_last_maintain.store(millis()); @@ -435,10 +468,12 @@ static void ethernet_task(void*) { // An online OFF is notified by the next mesh-loop maintain call, // allowing a command received over Ethernet to send its reply first. ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(20)); + if (ethernet_storage_conflict()) break; if (!ethernet_enabled.load()) break; if (ethernet_controller_faulted) continue; const bool maintain = ethernet_maintain_requested.exchange(false); ethernet_worker_take_access(); + if (ethernet_storage_conflict()) break; ethernet_poll_command_check(); if (ethernet_controller_faulted || ethernet_command_check_requested.load()) { // Preserve the maintenance request while command checking yields. @@ -446,6 +481,7 @@ static void ethernet_task(void*) { continue; } if (maintain) (void)Ethernet.maintain(); + if (ethernet_storage_conflict()) break; if (!ethernet_enabled.load()) break; if (maintain) ethernet_last_maintain.store(millis()); // Do not enter vendor accept/relisten while SEND/RECV is pending. @@ -465,10 +501,17 @@ static void ethernet_task(void*) { ethernet_worker_take_access(); ethernet_drain_before_stop(); ethernet_stop_hardware(); - ethernet_finish_task(EthernetCliState::Stopping); + ethernet_finish_task(ethernet_storage_conflict() && ethernet_enabled.load() + ? EthernetCliState::Faulted : EthernetCliState::Stopping); } static bool ethernet_request_start(char* reply, bool persist) { +#if defined(OTA_RAK15002_SD_STORE) + if (ethernet_storage_faulted.load() && ethernet_enabled.load()) { + strcpy(reply, "Error: SD insertion stopped Ethernet; turn Ethernet off first"); + return false; + } +#endif if (ethernet_state.load() == EthernetCliState::Stopping) { strcpy(reply, "Error: Ethernet is still stopping"); return false; @@ -489,6 +532,12 @@ static bool ethernet_request_start(char* reply, bool persist) { strcpy(reply, "Error: Ethernet bus safety hooks unavailable"); return false; } +#if defined(OTA_RAK15002_SD_STORE) + if (!ethernet_hooks.storage_conflict) { + strcpy(reply, "Error: Ethernet storage-conflict hook unavailable"); + return false; + } +#endif reply[0] = 0; if (!ethernet_hooks.prepare(ethernet_hooks.context, reply, 160)) { if (!reply[0]) strcpy(reply, "Error: Ethernet bus is busy"); @@ -510,6 +559,9 @@ static bool ethernet_request_start(char* reply, bool persist) { return false; } ethernet_enabled.store(true); +#if defined(OTA_RAK15002_SD_STORE) + ethernet_storage_faulted.store(false); +#endif ethernet_controller_faulted = false; ethernet_command_check_requested.store(false); ethernet_maintain_requested.store(false); diff --git a/src/helpers/nrf52/Rak4631SharedSpi.h b/src/helpers/nrf52/Rak4631SharedSpi.h index bd51ef786..4950286ea 100644 --- a/src/helpers/nrf52/Rak4631SharedSpi.h +++ b/src/helpers/nrf52/Rak4631SharedSpi.h @@ -1,8 +1,9 @@ #pragma once -#if defined(RAK4631_COMBINED_ETHERNET) +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) #include +#include namespace mesh { namespace ota { @@ -11,17 +12,41 @@ namespace ota { // controller at a time. Reserve them before starting Ethernet's DHCP task; // release them only after that task and every Ethernet socket have stopped. // NOR discovery and QSPI staging must never touch these pins while reserved. -inline std::atomic& rak4631_ethernet_spi_owner() { - static std::atomic owner{false}; +enum class RakSharedSpiOwner : uint8_t { None, Ethernet, Sd, Nor }; + +inline std::atomic& rak4631_shared_spi_owner() { + static std::atomic owner{RakSharedSpiOwner::None}; return owner; } +inline RakSharedSpiOwner rak4631_spi_owner() { + return rak4631_shared_spi_owner().load(std::memory_order_acquire); +} + +// Claims are main-loop lifetime reservations, not a recursive transaction +// lock. The same sole backend may confirm its retained claim; another device +// must never overwrite it, including on a failed mount or DHCP startup. +inline bool rak4631_try_acquire_spi(RakSharedSpiOwner wanted) { + if (wanted == RakSharedSpiOwner::None) return false; + auto expected = RakSharedSpiOwner::None; + return rak4631_shared_spi_owner().compare_exchange_strong( + expected, wanted, std::memory_order_acq_rel) || expected == wanted; +} + +inline void rak4631_release_spi(RakSharedSpiOwner owner) { + if (owner == RakSharedSpiOwner::None) return; + auto expected = owner; + (void)rak4631_shared_spi_owner().compare_exchange_strong( + expected, RakSharedSpiOwner::None, std::memory_order_acq_rel); +} + inline bool rak4631_ethernet_owns_spi() { - return rak4631_ethernet_spi_owner().load(std::memory_order_acquire); + return rak4631_spi_owner() == RakSharedSpiOwner::Ethernet; } inline void rak4631_set_ethernet_spi_owner(bool ethernet) { - rak4631_ethernet_spi_owner().store(ethernet, std::memory_order_release); + if (ethernet) (void)rak4631_try_acquire_spi(RakSharedSpiOwner::Ethernet); + else rak4631_release_spi(RakSharedSpiOwner::Ethernet); } } // namespace ota diff --git a/src/helpers/ota/OtaApply.cpp b/src/helpers/ota/OtaApply.cpp index 1e6834ca8..610a25cf4 100644 --- a/src/helpers/ota/OtaApply.cpp +++ b/src/helpers/ota/OtaApply.cpp @@ -466,7 +466,9 @@ bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, Ap static OtaStoreFlashNrf52* g_nrf52_apply_store = nullptr; #endif #if defined(OTA_QSPI_STORE) -static uint8_t g_nrf52_qspi_handoff = GPREGRET2_OTA_STAGE_QSPI; +// Set by the successfully verified external application path, never inferred +// from compile-time store flags: the same RAK image supports NOR and SD. +static uint8_t g_nrf52_external_handoff = GPREGRET2_OTA_STAGE_QSPI; #endif static void ota_nrf52_set_reset_handoff(uint8_t request, uint8_t source) { @@ -514,13 +516,13 @@ void ota_reboot_to_apply() { // public: set the apply magic + #if defined(OTA_TOWER_AUTO_STORE) stage_handoff = GPREGRET2_OTA_STAGE_SD; #else - stage_handoff = g_nrf52_qspi_handoff; + stage_handoff = g_nrf52_external_handoff; #endif } #elif defined(OTA_SD_STORE) stage_handoff = GPREGRET2_OTA_STAGE_SD; #elif defined(OTA_QSPI_STORE) - stage_handoff = g_nrf52_qspi_handoff; + stage_handoff = g_nrf52_external_handoff; #elif defined(OTA_FLASH_STORE) if (g_nrf52_apply_store && g_nrf52_apply_store->is_hybrid()) { // The bootloader admits retained SRAM only after a clean software-reset @@ -1155,9 +1157,21 @@ static bool ota_prepare_bootloader_update_external(Store& store, #if defined(OTA_SD_STORE) bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow, ApplyState& st, char* msg) { -#if defined(OTA_TOWER_AUTO_STORE) +#if defined(OTA_TOWER_AUTO_STORE) || defined(OTA_RAK_AUTO_STORE) g_nrf52_apply_store = nullptr; #endif +#if defined(OTA_RAK15002_SD_STORE) + OtaBootloaderIdentity rak_identity; + const OtaBlCaps rak_caps = ota_bootloader_app_caps(); + if (rak_caps.optional_sd_app_storage != OTA_BL_STORAGE_SD || + !ota_installed_bootloader_identity(rak_identity) || !rak_identity.crc_ok || + (strcmp(rak_identity.device_name, "4631_DFU") != 0 && + strcmp(rak_identity.device_name, "4631_AUTO_DFU") != 0)) { + st = ApplyState(); + strcpy(msg, "RAK SD application OTA requires the matching all-in-one OTAFIX bootloader"); + return false; + } +#endif #if defined(OTA_SD_BOOTLOADER_UPDATE) OtaBootloaderIdentity installed; if (!ota_installed_bootloader_identity(installed) || @@ -1170,7 +1184,12 @@ bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow, return false; } #endif - return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_SD, "SD", true, st, msg); + if (!ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_SD, "SD", true, st, msg)) + return false; +#if defined(OTA_RAK15002_SD_STORE) && defined(OTA_RAK_AUTO_STORE) + g_nrf52_external_handoff = GPREGRET2_OTA_STAGE_SD; +#endif + return true; } #if defined(OTA_SD_BOOTLOADER_UPDATE) @@ -1214,13 +1233,13 @@ bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store, const SignerAllowlist& allow #if defined(OTA_RAK_AUTO_STORE) g_nrf52_apply_store = nullptr; #endif - g_nrf52_qspi_handoff = GPREGRET2_OTA_STAGE_QSPI; + g_nrf52_external_handoff = GPREGRET2_OTA_STAGE_QSPI; if (!ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", false, st, msg)) { return false; } #if defined(OTA_RAK_AUTO_STORE) if (OtaStoreQspiNrf52::headerW25Detected()) { - g_nrf52_qspi_handoff = GPREGRET2_OTA_STAGE_HEADER_W25; + g_nrf52_external_handoff = GPREGRET2_OTA_STAGE_HEADER_W25; } #endif #if defined(OTA_RAK_AUTO_STORE) && !defined(RAK_3401) @@ -1228,7 +1247,7 @@ bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store, const SignerAllowlist& allow OtaBootloaderIdentity identity; if (ota_installed_bootloader_identity(identity) && identity.crc_ok && strcmp(identity.device_name, "4631_AUTO_DFU") == 0) { - g_nrf52_qspi_handoff = GPREGRET2_OTA_STAGE_RAK15001; + g_nrf52_external_handoff = GPREGRET2_OTA_STAGE_RAK15001; } } #endif @@ -1338,6 +1357,16 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store, ota_bl_optional_app_storage(image, OTA_BOOT_IMAGE_SIZE) != optional_storage) { strcpy(msg, "candidate must retain optional RAK application storage"); return false; } +#if defined(RAK_4631) || defined(OTA_RAK15002_SD_STORE) + const OtaBlCaps installed_app_caps = ota_bootloader_app_caps(); + if (installed_app_caps.optional_sd_app_storage != 0 && + ota_bl_rak_sd_app_storage(image, OTA_BOOT_IMAGE_SIZE, + candidate_caps.storage_flags, + ota_bl_optional_app_storage(image, OTA_BOOT_IMAGE_SIZE)) != + installed_app_caps.optional_sd_app_storage) { + strcpy(msg, "candidate must retain authenticated RAK SD application storage"); return false; + } +#endif #endif const OtaBootloaderContinuityGate continuity = ota_bootloader_continuity_gate( installed, candidate, m.fw_version, OTA_BOOT_CONTINUITY_FAMILY_S140, diff --git a/src/helpers/ota/OtaApply.h b/src/helpers/ota/OtaApply.h index 6da291377..830d1804e 100644 --- a/src/helpers/ota/OtaApply.h +++ b/src/helpers/ota/OtaApply.h @@ -94,6 +94,10 @@ bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len, uint32_t local_target_id, ApplyState& st, char* msg); #if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) class OtaStoreSdNrf52; +// Removable-media application apply always requires a trusted signature and +// contiguous staging plus retained authorization. RAK15002 additionally needs +// the exact supplemental all-in-one loader capability; SD never stages its +// bootloader packages, which continue through the internal-store overload. bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow, ApplyState& st, char* msg); #if defined(OTA_SD_BOOTLOADER_UPDATE) diff --git a/src/helpers/ota/OtaBlInfo.h b/src/helpers/ota/OtaBlInfo.h index 74cf0a371..734f96963 100644 --- a/src/helpers/ota/OtaBlInfo.h +++ b/src/helpers/ota/OtaBlInfo.h @@ -29,6 +29,7 @@ struct OtaBlCaps { uint16_t codec_mask = 0; // bit i set => can apply codec_id i (in-place delta = bit 2) uint8_t storage_flags = 0; // OTA_BL_STORAGE_* capability bits uint8_t optional_app_storage = 0; // separate MOTASTOR record, scoped to its qualified primary profile + uint8_t optional_sd_app_storage = 0; // supplemental MOTASDRA; application-only RAK15002 support }; // Independent marker for the reset-retained hybrid source. Keep this separate @@ -256,12 +257,37 @@ inline uint8_t ota_bl_profile_optional_app_storage(const uint8_t* bytes, return 0; } +// The all-in-one RAK4631 loader deliberately keeps its deployed internal/NOR +// capability bytes unchanged. SD application apply is a separate, exact ABI; +// it never grants SD bootloader updates. Count every aligned magic, including +// malformed/truncated declarations, so a second record cannot hide a conflict. +inline uint8_t ota_bl_rak_sd_app_storage(const uint8_t* bytes, size_t len, + uint8_t primary_storage, + uint8_t optional_storage) { + static const uint8_t marker[16] = { + 'M', 'O', 'T', 'A', 'S', 'D', 'R', 'A', 1, 0, 72, 0, 1, 0x53, 0, 0, + }; + if (!bytes || primary_storage != OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE || + optional_storage != (OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_HEADER_W25)) return 0; + unsigned matches = 0; + for (size_t off = 0; off + 8u <= len; off += 4u) { + if (memcmp(bytes + off, marker, 8u) != 0) continue; + if (++matches != 1u || off + sizeof(marker) > len || + memcmp(bytes + off, marker, sizeof(marker)) != 0) return 0; + } + return matches == 1u ? OTA_BL_STORAGE_SD : 0; +} + inline OtaBlCaps ota_bl_app_caps_scan(const uint8_t* bytes, size_t len) { OtaBlCaps aligned = ota_bl_caps_scan_aligned(bytes, len, false); if (aligned.present && aligned.apply_abi >= 3) { aligned.optional_app_storage = ota_bl_profile_optional_app_storage( bytes, len, aligned.storage_flags); - aligned.storage_flags |= aligned.optional_app_storage; +#if !defined(NRF52_PLATFORM) || defined(RAK_4631) || defined(OTA_RAK15002_SD_STORE) + aligned.optional_sd_app_storage = ota_bl_rak_sd_app_storage( + bytes, len, aligned.storage_flags, aligned.optional_app_storage); +#endif + aligned.storage_flags |= aligned.optional_app_storage | aligned.optional_sd_app_storage; } if (aligned.present) return aligned; return ota_bl_legacy_app_caps_scan_halfword(bytes, len); @@ -276,6 +302,10 @@ inline OtaBlCaps ota_bl_update_caps_scan_aligned(const uint8_t* bytes, size_t le // require an existing optional application capability in a successor. caps.optional_app_storage = ota_bl_profile_optional_app_storage( bytes, len, caps.storage_flags); +#if !defined(NRF52_PLATFORM) || defined(RAK_4631) || defined(OTA_RAK15002_SD_STORE) + caps.optional_sd_app_storage = ota_bl_rak_sd_app_storage( + bytes, len, caps.storage_flags, caps.optional_app_storage); +#endif } return caps; } @@ -317,7 +347,15 @@ inline OtaBlCaps ota_bootloader_update_caps() { #if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) && defined(OTA_RAK_AUTO_STORE) // Unified RAK applications support the existing internal board identity as // well as the adaptive successor. Both require one privileged marker. - const OtaBlCaps caps = ota_bl_caps_scan_aligned(lo, (size_t)(hi - lo), true); + OtaBlCaps caps = ota_bl_caps_scan_aligned(lo, (size_t)(hi - lo), true); + if (caps.present) { + caps.optional_app_storage = ota_bl_profile_optional_app_storage( + lo, (size_t)(hi - lo), caps.storage_flags); +#if defined(RAK_4631) || defined(OTA_RAK15002_SD_STORE) + caps.optional_sd_app_storage = ota_bl_rak_sd_app_storage( + lo, (size_t)(hi - lo), caps.storage_flags, caps.optional_app_storage); +#endif + } return ota_bootloader_self_update_caps_valid(caps) ? caps : OtaBlCaps(); #else return ota_bl_update_caps_scan_aligned( diff --git a/src/helpers/ota/OtaBootloaderUpdate.h b/src/helpers/ota/OtaBootloaderUpdate.h index 22fb385d1..c14400300 100644 --- a/src/helpers/ota/OtaBootloaderUpdate.h +++ b/src/helpers/ota/OtaBootloaderUpdate.h @@ -40,7 +40,8 @@ #error "OTA_INTERNAL_BOOTLOADER_UPDATE requires nRF52 internal-flash staging" #endif #if (defined(OTA_QSPI_STORE) && !defined(OTA_RAK_AUTO_STORE)) || \ - defined(OTA_SD_STORE) || defined(QSPIFLASH) + (defined(OTA_SD_STORE) && !(defined(OTA_RAK15002_SD_STORE) && \ + defined(OTA_RAK_AUTO_STORE) && defined(RAK_4631))) || defined(QSPIFLASH) #error "internal bootloader staging cannot share an external OTA/filesystem store" #endif #endif diff --git a/src/helpers/ota/OtaCacheSdNrf52.cpp b/src/helpers/ota/OtaCacheSdNrf52.cpp index 41839a882..6ebbb530e 100644 --- a/src/helpers/ota/OtaCacheSdNrf52.cpp +++ b/src/helpers/ota/OtaCacheSdNrf52.cpp @@ -18,7 +18,7 @@ namespace ota { const char* const OtaCacheSdNrf52::DIR = "/mota"; const char* const OtaCacheSdNrf52::DISABLED_PATH = "/mota/cache.off"; -OtaCacheSdNrf52::OtaCacheSdNrf52() : _file(new (std::nothrow) FsFile()) {} +OtaCacheSdNrf52::OtaCacheSdNrf52() : _file(new (std::nothrow) OtaSdFile()) {} OtaCacheSdNrf52::~OtaCacheSdNrf52() { if (_file) { _file->close(); delete _file; } @@ -58,6 +58,9 @@ bool OtaCacheSdNrf52::ensureDir() { } bool OtaCacheSdNrf52::initialize() { +#if defined(OTA_RAK15002_SD_STORE) + if (!ready()) return false; +#endif if (_initialized) return true; _error[0] = 0; if (!ready() || !ensureDir()) return false; @@ -72,7 +75,7 @@ static int cache_hex_nibble(char c) { return -1; } -bool OtaCacheSdNrf52::describeFile(FsFile& file, MotaDesc& out) const { +bool OtaCacheSdNrf52::describeFile(OtaSdFile& file, MotaDesc& out) const { uint64_t size64 = file.fileSize(); if (size64 < 8 + MOTA_MFL + 5 || size64 > UINT32_MAX) return false; const uint32_t total = (uint32_t)size64; @@ -111,9 +114,9 @@ bool OtaCacheSdNrf52::rescan() { _count = 0; if (!ready() || !ensureDir()) return false; - FsFile dir = _owner->_sd->open(DIR, O_RDONLY); + OtaSdFile dir = _owner->_sd->open(DIR, O_RDONLY); if (!dir || !dir.isDir()) { fail("SD archive directory open failed"); return false; } - FsFile file; + OtaSdFile file; while (_count < OTA_SD_CACHE_MAX && file.openNext(&dir, O_RDONLY)) { if (!file.isDir()) { char name[24] = {0}; @@ -156,7 +159,7 @@ bool OtaCacheSdNrf52::contains(const uint8_t mid[4]) { // never enough: malformed files must remain eligible for quarantine and a clean re-download. char path[32]; pathFor(mid, "mota", path, sizeof(path)); - FsFile file = _owner->_sd->open(path, O_RDONLY); + OtaSdFile file = _owner->_sd->open(path, O_RDONLY); MotaDesc d; bool valid = file && describeFile(file, d) && memcmp(d.mid, mid, 4) == 0; file.close(); @@ -188,7 +191,7 @@ bool OtaCacheSdNrf52::setAutoCaptureEnabled(bool enabled) { return false; } } else { - FsFile marker = _owner->_sd->open(DISABLED_PATH, O_WRONLY | O_CREAT | O_TRUNC); + OtaSdFile marker = _owner->_sd->open(DISABLED_PATH, O_WRONLY | O_CREAT | O_TRUNC); static const uint8_t off[] = {'o', 'f', 'f', '\n'}; if (!marker || marker.write(off, sizeof(off)) != sizeof(off) || !marker.sync()) { marker.close(); @@ -295,6 +298,9 @@ bool OtaCacheSdNrf52::set_meta_size(uint32_t meta_bytes) { } bool OtaCacheSdNrf52::write(uint32_t off, const uint8_t* data, uint32_t len) { +#if defined(OTA_RAK15002_SD_STORE) + if (!_owner || !_owner->busAccessAllowed()) return false; +#endif if (!_file || !*_file || (uint64_t)off + len > _total || !_file->seekSet(off) || _file->write(data, len) != len) { fail("SD archive write failed"); @@ -304,17 +310,26 @@ bool OtaCacheSdNrf52::write(uint32_t off, const uint8_t* data, uint32_t len) { } bool OtaCacheSdNrf52::read(uint32_t off, uint8_t* buf, uint32_t len) const { +#if defined(OTA_RAK15002_SD_STORE) + if (!_owner || !_owner->busAccessAllowed()) return false; +#endif return _file && *_file && (uint64_t)off + len <= _total && _file->seekSet(off) && _file->read(buf, len) == (int)len; } uint32_t OtaCacheSdNrf52::capacity() const { +#if defined(OTA_RAK15002_SD_STORE) + if (!_owner || !_owner->busAccessAllowed()) return 0; +#endif if (!_owner || !_owner->_mounted || !_owner->_sd || !_owner->_sd->card()) return 0; uint64_t bytes = (uint64_t)_owner->_sd->card()->sectorCount() * 512; return bytes > UINT32_MAX ? UINT32_MAX : (uint32_t)bytes; } void OtaCacheSdNrf52::checkpoint() { +#if defined(OTA_RAK15002_SD_STORE) + if (!_owner || !_owner->busAccessAllowed()) return; +#endif if (_file && *_file) { _file->sync(); if (_owner && _owner->_sd->card()) _owner->_sd->card()->syncDevice(); @@ -322,6 +337,9 @@ void OtaCacheSdNrf52::checkpoint() { } bool OtaCacheSdNrf52::finalize() { +#if defined(OTA_RAK15002_SD_STORE) + if (!_owner || !_owner->busAccessAllowed()) return false; +#endif if (!_file || !*_file || !_total || !_file->sync()) { fail("SD archive sync failed"); return false; } if (_owner->_sd->card() && !_owner->_sd->card()->syncDevice()) { fail("SD archive card sync failed"); @@ -332,7 +350,7 @@ bool OtaCacheSdNrf52::finalize() { pathFor(_mid, "part", part_path, sizeof(part_path)); _file->close(); - FsFile check = _owner->_sd->open(part_path, O_RDONLY); + OtaSdFile check = _owner->_sd->open(part_path, O_RDONLY); MotaDesc d; bool valid = check && describeFile(check, d) && memcmp(d.mid, _mid, 4) == 0; check.close(); @@ -396,11 +414,16 @@ bool OtaCacheSdNrf52::describe(uint8_t idx, MotaDesc& out) { } bool OtaCacheSdNrf52::read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len) { - if (!_initialized || idx >= _count || (uint64_t)off + len > _entries[idx].total_size || !ready()) - return false; + if (!_initialized || idx >= _count) return false; +#if defined(OTA_RAK15002_SD_STORE) + const uint32_t total_size = _entries[idx].totalSize(); +#else + const uint32_t total_size = _entries[idx].total_size; +#endif + if ((uint64_t)off + len > total_size || !ready()) return false; char path[32]; pathFor(_entries[idx].mid, "mota", path, sizeof(path)); - FsFile file = _owner->_sd->open(path, O_RDONLY); + OtaSdFile file = _owner->_sd->open(path, O_RDONLY); bool ok = file && file.seekSet(off) && file.read(buf, len) == (int)len; file.close(); return ok; diff --git a/src/helpers/ota/OtaCacheSdNrf52.h b/src/helpers/ota/OtaCacheSdNrf52.h index b8595767e..79aeaf7ae 100644 --- a/src/helpers/ota/OtaCacheSdNrf52.h +++ b/src/helpers/ota/OtaCacheSdNrf52.h @@ -4,8 +4,7 @@ #include "OtaSource.h" #include "OtaStore.h" - -class FsFile; +#include "OtaSdTypesNrf52.h" namespace mesh { namespace ota { @@ -23,7 +22,7 @@ class OtaStoreSdNrf52; // Persistent capture destination and MotaSource for the SD OTA archive. Completed containers live as // /mota/.mota; an in-progress capture uses /mota/.part and can resume after interruption. -// This object shares the staging store's mounted SdFs volume so FAT metadata never has two independent +// This object shares the staging store's mounted volume so FAT metadata never has two independent // caches for the same card. class OtaCacheSdNrf52 : public OtaStore, public MotaSource { public: @@ -68,14 +67,48 @@ private: bool ready(); bool ensureDir(); - bool describeFile(FsFile& file, MotaDesc& out) const; + bool describeFile(OtaSdFile& file, MotaDesc& out) const; bool quarantineFinal(const uint8_t mid[4]); void pathFor(const uint8_t mid[4], const char* ext, char* out, size_t cap) const; void fail(const char* message); OtaStoreSdNrf52* _owner = nullptr; - FsFile* _file = nullptr; + OtaSdFile* _file = nullptr; +#if defined(OTA_RAK15002_SD_STORE) + // Valid archive files all have the fixed manifest followed immediately by + // leaves and payload. Keep every descriptor field without caching three + // redundant offsets/sizes per entry (192 bytes saved at 16 entries). + struct CachedDesc { + uint8_t mid[4]; + uint32_t target_id, fw_version, block_count, payload_size; + uint8_t codec_id, flags, block_size_log2, source_caps; + CachedDesc& operator=(const MotaDesc& d) { + for (unsigned i = 0; i < 4; ++i) mid[i] = d.mid[i]; + target_id = d.target_id; fw_version = d.fw_version; + block_count = d.block_count; payload_size = d.payload_size; + codec_id = d.codec_id; flags = d.flags; + block_size_log2 = d.block_size_log2; source_caps = d.source_caps; + return *this; + } + uint32_t totalSize() const { return 8u + MOTA_MFL + block_count * 4u + payload_size + 5u; } + operator MotaDesc() const { + MotaDesc d; + for (unsigned i = 0; i < 4; ++i) d.mid[i] = mid[i]; + d.target_id = target_id; d.fw_version = fw_version; + d.block_count = block_count; d.payload_size = payload_size; + d.codec_id = codec_id; d.flags = flags; + d.block_size_log2 = block_size_log2; d.source_caps = source_caps; + d.leaves_off = 8u + MOTA_MFL; + d.payload_off = d.leaves_off + block_count * 4u; + d.total_size = totalSize(); + return d; + } + }; + static_assert(sizeof(CachedDesc) == 24u, "bounded RAK SD index changed"); + CachedDesc _entries[OTA_SD_CACHE_MAX]; +#else MotaDesc _entries[OTA_SD_CACHE_MAX]; +#endif uint8_t _count = 0; uint8_t _mid[4] = {0}; uint32_t _total = 0; diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index 2a41316d0..39153b038 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -200,7 +200,7 @@ static __attribute__((noinline)) bool handle_status(char* reply, OtaContext& c) c.fetch_store.usesInternal() ? "internal" : "storage-ERR"; #elif defined(OTA_RAK_AUTO_STORE) const char* bl_state = !bl.present ? "NONE" : - c.fetch_store.usesExternal() ? "QSPI" : + c.fetch_store.usesSd() ? "SD" : c.fetch_store.usesQspi() ? "QSPI" : c.fetch_store.usesInternal() ? "internal" : "storage-ERR"; #elif defined(OTA_QSPI_STORE) const char* bl_state = !bl.present ? "NONE" : @@ -267,7 +267,8 @@ static __attribute__((noinline)) bool handle_neighbors( const OtaBlCaps& list_bl_update = c.bootloaderUpdateCaps(); #endif #endif -#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \ +#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && \ + (!defined(OTA_SD_STORE) || defined(OTA_RAK15002_SD_STORE)) && \ (!defined(OTA_QSPI_STORE) || defined(OTA_RAK_AUTO_STORE)) SelfFwInfo list_self; bool list_has_endf = ota_self_firmware(list_self) && list_self.valid; @@ -337,11 +338,17 @@ static __attribute__((noinline)) bool handle_neighbors( installable = installable && c.fetch_store.compatible(); if (h->codec == CODEC_FULL) installable = installable && c.fetch_store.usesExternal(); -#elif defined(OTA_SD_STORE) +#elif defined(OTA_SD_STORE) && !defined(OTA_RAK_AUTO_STORE) installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_SD); #elif defined(OTA_RAK_AUTO_STORE) - if (c.fetch_store.usesExternal()) + if (c.fetch_store.usesQspi()) installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_QSPI); +#if defined(OTA_RAK15002_SD_STORE) + if (c.fetch_store.usesSd()) + installable = installable && list_bl.optional_sd_app_storage == OTA_BL_STORAGE_SD && + (h->flags & MFLAG_SIGNED); +#endif + installable = installable && (c.fetch_store.usesExternal() || c.fetch_store.usesInternal()); if (h->codec == CODEC_FULL) installable = installable && c.fetch_store.usesExternal(); #elif defined(OTA_QSPI_STORE) @@ -388,6 +395,11 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board #endif const char* rest = a; if (*a == 0 || is_cmd(a, "status|st", &rest)) return handle_status(reply, c); +#if defined(NRF52_PLATFORM) && defined(OTA_RAK15002_SD_STORE) && !defined(OTA_SEEDER_ONLY) + // Radio/USB startup and status remain independent of removable storage. + // Explicit transfer/catalog/cache work may prepare it at this main-loop boundary. + if (!is_cmd(a, "qspi|storage|self|id|stats|help|?|h", &rest)) c.prepareRakStorage(); +#endif if (is_cmd(a, "neighbors|nbrs|updates|ls|n", &rest)) return handle_neighbors(rest, reply, c); return handle_control_command(a, reply, board, c); @@ -425,7 +437,11 @@ static __attribute__((noinline)) bool handle_control_command( #endif #elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) strcpy(reply, - "OTA: status | stats | ls | get flash | install | bootloader | cancel | announce | self | serve | storage | folder | config | key"); + "OTA: status | stats | ls | get flash | install | bootloader | cancel | announce | self | serve | storage | folder | " +#if defined(OTA_RAK15002_SD_STORE) + "cache | " +#endif + "config | key"); #elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) #if defined(OTA_QSPI_BOOTLOADER_UPDATE) strcpy(reply, @@ -658,7 +674,7 @@ static __attribute__((noinline)) bool handle_control_command( snprintf(reply, 160, "ERR OTA storage unsafe: %s", c.fetch_store.selectionReason()); return true; } -#elif defined(OTA_SD_STORE) +#elif defined(OTA_SD_STORE) && !defined(OTA_RAK_AUTO_STORE) if (!(bl.storage_flags & OTA_BL_STORAGE_SD)) { strcpy(reply, "ERR bootloader cannot apply an update staged on SD; update it over USB first"); return true; @@ -667,10 +683,26 @@ static __attribute__((noinline)) bool handle_control_command( // Bootloader packages passed their separate privileged checks above and // select internal staging when their manifest arrives. The currently // selected application backend must not veto that transition. - if (!selboot && c.fetch_store.usesExternal() && !(bl.storage_flags & OTA_BL_STORAGE_QSPI)) { + if (!selboot && +#if defined(OTA_RAK15002_SD_STORE) + c.fetch_store.usesQspi() && +#else + c.fetch_store.usesExternal() && +#endif + !(bl.storage_flags & OTA_BL_STORAGE_QSPI)) { strcpy(reply, "ERR bootloader cannot apply from detected QSPI storage"); return true; } +#if defined(OTA_RAK15002_SD_STORE) + if (!selboot && c.fetch_store.usesSd() && bl.optional_sd_app_storage != OTA_BL_STORAGE_SD) { + strcpy(reply, "ERR bootloader lacks authenticated RAK SD application support"); + return true; + } + if (!selboot && c.fetch_store.usesSd() && !(selflags & MFLAG_SIGNED)) { + strcpy(reply, "ERR SD application install requires a trusted signed package; use folder to inspect"); + return true; + } +#endif if (!selboot && !c.fetch_store.usesExternal() && !c.fetch_store.usesInternal()) { snprintf(reply, 160, "ERR OTA storage unsafe: %s", c.fetch_store.selectionReason()); return true; @@ -682,7 +714,8 @@ static __attribute__((noinline)) bool handle_control_command( } #endif #endif -#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \ +#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && \ + (!defined(OTA_SD_STORE) || defined(OTA_RAK15002_SD_STORE)) && \ (!defined(OTA_QSPI_STORE) || defined(OTA_RAK_AUTO_STORE)) SelfFwInfo self; bool has_endf = ota_self_firmware(self) && self.valid; @@ -765,6 +798,11 @@ static __attribute__((noinline)) bool handle_control_command( #endif const bool discarded = !was_folder && !was_sd_archive && c.fetch_store.discard(); +#if defined(NRF52_PLATFORM) && defined(OTA_RAK15002_SD_STORE) && !defined(OTA_SEEDER_ONLY) + // Keep cache sources/independent serving mounted; only refresh the + // application policy after the selected staged file was invalidated. + c.prepareRakStorage(); +#endif #if defined(NRF52_PLATFORM) && defined(OTA_TOWER_AUTO_STORE) if (discarded || (!c.fetch_store.usesExternal() && !c.fetch_store.usesInternal())) { c.fetch_store.resetSelection(); @@ -835,7 +873,13 @@ static __attribute__((noinline)) bool handle_control_command( snprintf(reply, 160, "OTA storage: %s; capacity=%luK; SD primary, internal application deltas only", c.fetch_store.selectionReason(), (unsigned long)(c.fetch_store.capacity() / 1024)); #elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) - if (c.fetch_store.usesExternal()) { +#if defined(OTA_RAK15002_SD_STORE) + if (c.fetch_store.usesSd()) { + snprintf(reply, 160, "OTA storage: RAK15002 SD; staged=%lu; use get sdcard free for media space", + (unsigned long)c.fetch_store.sdStore().staged_size()); + } else +#endif + if (c.fetch_store.usesQspi()) { const uint32_t qspi_capacity = c.fetch_store.qspiCapacity(); snprintf(reply, 160, "QSPI %s jedec=%06lX size=%luK sr1=%02X stage=%s%s%s", c.fetch_store.selectionReason(), (unsigned long)c.fetch_store.jedec_id(), @@ -843,16 +887,26 @@ static __attribute__((noinline)) bool handle_control_command( c.fetch_store.last_stage(), c.fetch_store.last_error()[0] ? " error=" : "", c.fetch_store.last_error()); } else if (c.fetch_store.usesInternal()) { +#if defined(OTA_RAK15002_SD_STORE) + snprintf(reply, 160, "OTA storage: %s; internal capacity=%luK", + c.fetch_store.selectionReason(), (unsigned long)(c.fetch_store.capacity() / 1024)); +#else uint32_t rak_id = 0, w25_id = 0; OtaStoreQspiNrf52::autoProbeIds(rak_id, w25_id); snprintf(reply, 160, "OTA storage: %s; internal capacity=%luK; probe rak=%06lX w25=%06lX", c.fetch_store.selectionReason(), (unsigned long)(c.fetch_store.capacity() / 1024), (unsigned long)rak_id, (unsigned long)w25_id); +#endif } else { +#if defined(OTA_RAK15002_SD_STORE) + snprintf(reply, 160, "OTA storage: %s; optional storage is prepared by OTA transfer/cache work", + c.fetch_store.selectionReason()); +#else uint32_t rak_id = 0, w25_id = 0; OtaStoreQspiNrf52::autoProbeIds(rak_id, w25_id); snprintf(reply, 160, "OTA storage unsafe: %s; probe rak=%06lX w25=%06lX", c.fetch_store.selectionReason(), (unsigned long)rak_id, (unsigned long)w25_id); +#endif } #elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) // This probe only reads JEDEC/SR1. capacity() deliberately preserves a @@ -887,7 +941,13 @@ static __attribute__((noinline)) bool handle_control_command( c.fetch_store.usesExternal() ? "SD" : c.fetch_store.usesInternal() ? "internal" : "unsafe", c.fetch_store.compatible() ? "apply OK" : "unsupported"); #elif defined(OTA_RAK_AUTO_STORE) - if (c.fetch_store.usesExternal()) { +#if defined(OTA_RAK15002_SD_STORE) + if (c.fetch_store.usesSd()) { + snprintf(reply + n, 160 - n, " | SD store | bootloader: %s", + bl.optional_sd_app_storage == OTA_BL_STORAGE_SD ? "authenticated SD apply OK" : "NO SD apply"); + } else +#endif + if (c.fetch_store.usesQspi()) { const uint32_t qspi_capacity = c.fetch_store.qspiCapacity(); snprintf(reply + n, 160 - n, " | QSPI store:%s%uK | bootloader: %s", qspi_capacity ? "" : "ERR ", @@ -1395,6 +1455,9 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) { strcpy(reply, "OK cleared SD archive session; partial retained for resume"); } else { const bool discarded = c.fetch_store.discard(); +#if defined(NRF52_PLATFORM) && defined(OTA_RAK15002_SD_STORE) && !defined(OTA_SEEDER_ONLY) + c.prepareRakStorage(); +#endif #if defined(NRF52_PLATFORM) && defined(OTA_TOWER_AUTO_STORE) if (discarded || (!c.fetch_store.usesExternal() && !c.fetch_store.usesInternal())) { c.fetch_store.resetSelection(); diff --git a/src/helpers/ota/OtaContext.h b/src/helpers/ota/OtaContext.h index 9329347a7..64a9f16ec 100644 --- a/src/helpers/ota/OtaContext.h +++ b/src/helpers/ota/OtaContext.h @@ -33,6 +33,9 @@ #include "OtaCacheSdNrf52.h" #elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) #include "OtaStoreAdaptiveNrf52.h" + #if defined(OTA_RAK15002_SD_STORE) + #include "OtaCacheSdNrf52.h" + #endif #elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) #include "OtaStoreQspiNrf52.h" #elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) @@ -43,6 +46,9 @@ #elif defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE) #include "OtaStoreFlashEsp32.h" #endif +#if defined(OTA_RAK15002_SD_STORE) + #include "../nrf52/Rak4631SharedSpi.h" +#endif #if defined(OTA_FOLDER_SERIAL) #include "MotaSourceSerial.h" // relay an external folder served by a host daemon over the USB serial #include "../UsbLogging.h" @@ -125,6 +131,9 @@ struct OtaContext { OtaCacheSdNrf52 sd_cache; // always owns the stable SD store #elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) OtaStoreAdaptiveNrf52 fetch_store; + #if defined(OTA_RAK15002_SD_STORE) + OtaCacheSdNrf52 sd_cache; + #endif #elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) OtaStoreQspiNrf52 fetch_store; // persistent raw QSPI staging, full + in-place delta #elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) @@ -336,7 +345,17 @@ struct OtaContext { } } bool ok; -#if defined(NRF52_PLATFORM) && (defined(OTA_RAK_AUTO_STORE) || defined(OTA_TOWER_AUTO_STORE)) +#if defined(NRF52_PLATFORM) && defined(OTA_RAK15002_SD_STORE) && defined(OTA_RAK_AUTO_STORE) + ok = fetch_store.usesSd() + ? ota_apply_mota_nrf52(fetch_store.sdStore(), allow, apply_st, msg) + : fetch_store.usesQspi() + ? ota_apply_mota_nrf52(fetch_store.externalStore(), allow, apply_st, msg) + : fetch_store.usesInternal() + ? ota_apply_mota_nrf52(fetch_store.internalStore(), allow, apply_st, msg) + : false; + if (!fetch_store.usesExternal() && !fetch_store.usesInternal()) + strncpy(msg, fetch_store.selectionReason(), 96); +#elif defined(NRF52_PLATFORM) && (defined(OTA_RAK_AUTO_STORE) || defined(OTA_TOWER_AUTO_STORE)) ok = fetch_store.usesExternal() ? ota_apply_mota_nrf52(fetch_store.externalStore(), allow, apply_st, msg) : fetch_store.usesInternal() @@ -685,12 +704,80 @@ struct OtaContext { } #endif +#if defined(OTA_RAK15002_SD_STORE) + bool canReleaseRakSdForEthernet() const { + const bool own_image = manager.servingPrimaryManifest(serve_self_manifest); + return manager.fetchState() == OtaManager::IDLE && fetch_store.staged_size() == 0u && + !apply_pending && !bootloader_apply_pending && !_sd_cache_fetching && + !manager.pendingServeJobs() && !manager.pendingManifestJobs() && + (!serving || own_image) && serve_expected == 0u && !folder_active && + !_folder_source && !folder_dest && !disconnected_folder_dest && !fetch_to_folder; + } + + bool prepareRakSdForEthernet() { + if (!canReleaseRakSdForEthernet()) return false; + if (_sd_cache_source_attached) manager.remove_source(&sd_cache); + _sd_cache_source_attached = false; + sd_cache.resetMedia(); + if (!sdStagingStore().releaseBus()) return false; +#if !defined(OTA_SEEDER_ONLY) + if (!fetch_store.resetSelection()) return false; + manager.set_accept_full(false); +#endif + manager.clear_primary(); + serving = false; + free(serve_self_leaves); free(serve_self_proof); + serve_self_leaves = serve_self_proof = nullptr; + releaseServeBuffer(); + serve_expected = 0; + manager.set_archive_interest(false); + _sd_cache_init_retry_at = 0; + return true; + } + + bool releasedRakSdEthernet() { + if (!canReleaseRakSdForEthernet()) return false; +#if !defined(OTA_SEEDER_ONLY) + if (!fetch_store.resetSelection()) return false; + manager.set_accept_full(false); +#endif + manager.set_archive_interest(true); + _sd_cache_init_retry_at = 0; + return true; // The next normal main-loop service may probe, never this callback. + } +#endif + +#if defined(OTA_RAK15002_SD_STORE) && !defined(OTA_SEEDER_ONLY) + // Only actual OTA work may probe the optional card. Boot/status paths leave + // the selector untouched, and a selected transfer never changes its medium. + bool prepareRakStorage() { + const bool usable = fetch_store.selectStorage(); + const bool external = usable && fetch_store.usesExternal() && !rak4631_ethernet_owns_spi(); + manager.set_accept_full(external); + manager.set_accept_bootloader(ota_bootloader_self_update_caps_valid(bootloaderUpdateCaps())); + const bool was_self_serve_supported = self_serve_supported; + self_serve_supported = ota_self_serve_supported(external); + if (was_self_serve_supported && !self_serve_supported && + manager.servingPrimaryManifest(serve_self_manifest)) { + manager.clear_primary(); + serving = false; + } +#if MESHCORE_OTA_DEVICE_DEFLATE + manager.set_transport_deflate_encoder(self_serve_supported ? ota_transport_deflate : nullptr); +#endif + if (!fetch_store.usesSd()) manager.set_archive_interest(false); + return usable; + } +#endif + #if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) OtaStoreSdNrf52& sdStagingStore() { #if defined(OTA_SEEDER_ONLY) return sd_archive_store; #elif defined(OTA_TOWER_AUTO_STORE) return fetch_store.sdStore(); +#elif defined(OTA_RAK15002_SD_STORE) && defined(OTA_RAK_AUTO_STORE) + return fetch_store.sdStore(); #else return fetch_store; #endif @@ -723,6 +810,12 @@ struct OtaContext { // Initialize and attach the persistent source lazily. This lets Mesh::begin finish quickly when no card // is inserted, while archive interest is already enabled so early OTA advertisements still get queried. bool ensureSdCache() { +#if defined(OTA_RAK15002_SD_STORE) + if (rak4631_ethernet_owns_spi()) return false; +#if !defined(OTA_SEEDER_ONLY) + if (!prepareRakStorage() || !fetch_store.usesSd()) return false; +#endif +#endif sd_cache.attach(sdStagingStore()); #if defined(OTA_TOWER_AUTO_STORE) if (!prepareTowerStorage() || !fetch_store.usesExternal()) return false; @@ -778,12 +871,20 @@ struct OtaContext { sd_cache.resetMedia(); #if defined(OTA_TOWER_AUTO_STORE) fetch_store.resetSelection(); +#elif defined(OTA_RAK15002_SD_STORE) && !defined(OTA_SEEDER_ONLY) + if (!fetch_store.resetSelection()) return; #endif _sd_cache_init_retry_at = now; ensureSdCache(); } void serviceSdCache(uint32_t now) { +#if defined(OTA_RAK15002_SD_STORE) + if (rak4631_ethernet_owns_spi()) return; +#if !defined(OTA_SEEDER_ONLY) + if (!prepareRakStorage() || !fetch_store.usesSd()) return; +#endif +#endif #if defined(OTA_TOWER_AUTO_STORE) if (!prepareTowerStorage() || !fetch_store.usesExternal()) { if (_sd_cache_source_attached) manager.remove_source(&sd_cache); @@ -897,7 +998,11 @@ struct OtaContext { manager.set_accept_full(false); manager.set_apply_codec(CODEC_DETOOLS_INPLACE); #elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) +#if defined(OTA_RAK15002_SD_STORE) + manager.set_accept_full(false); // defer optional SD/NOR probing until startup finishes +#else manager.set_accept_full(fetch_store.usesExternal()); +#endif manager.set_apply_codec(CODEC_DETOOLS_INPLACE); #elif defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)) manager.set_accept_full(true); @@ -925,7 +1030,8 @@ struct OtaContext { manager.set_accept_bootloader(false); #endif manager.set_fetch_store(&fetch_store); -#if defined(NRF52_PLATFORM) && defined(OTA_TOWER_AUTO_STORE) +#if defined(NRF52_PLATFORM) && (defined(OTA_TOWER_AUTO_STORE) || \ + (defined(OTA_RAK15002_SD_STORE) && !defined(OTA_SEEDER_ONLY))) self_serve_supported = false; #elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) && !defined(OTA_SEEDER_ONLY) self_serve_supported = ota_self_serve_supported(fetch_store.usesExternal()); diff --git a/src/helpers/ota/OtaSdSpiBusNrf52.h b/src/helpers/ota/OtaSdSpiBusNrf52.h index 2182c6fcb..1c54f5a12 100644 --- a/src/helpers/ota/OtaSdSpiBusNrf52.h +++ b/src/helpers/ota/OtaSdSpiBusNrf52.h @@ -2,6 +2,17 @@ #include +#if defined(OTA_RAK15002_SD_CACHE) && defined(OTA_RAK15002_SD_STORE) + #error "RAK15002 seeder-only cache and adaptive SD receiver profiles are distinct" +#endif + +#if defined(OTA_RAK15002_SD_STORE) + #if !defined(NRF52_PLATFORM) || !defined(RAK_4631) || !defined(OTA_SD_STORE) + #error "RAK15002 adaptive SD requires the exact nRF52 RAK4631 SD profile" + #endif + #include "../nrf52/Rak4631SharedSpi.h" +#endif + #if defined(OTA_RAK15002_SD_CACHE) #if !defined(NRF52_PLATFORM) || !defined(RAK_4631) || !defined(OTA_SD_STORE) || !defined(OTA_SEEDER_ONLY) #error "RAK15002 SD cache requires the exact nRF52 RAK4631 seeder-only profile" @@ -9,10 +20,12 @@ #if defined(ETHERNET_ENABLED) || defined(OTA_QSPI_STORE) || defined(OTA_RAK_AUTO_STORE) || defined(OTA_TOWER_AUTO_STORE) #error "RAK15002 SD cache cannot share the WisBlock SPI/CS with Ethernet or OTA NOR" #endif +#endif +#if defined(OTA_RAK15002_SD_CACHE) || defined(OTA_RAK15002_SD_STORE) #if SPI_INTERFACES_COUNT != 1 || (defined(SPI_32MHZ_INTERFACE) && SPI_32MHZ_INTERFACE != 0) #error "RAK15002 SD cache requires radio SPI on SPIM3 and reserves SPIM2 for the SD card" #endif - #if defined(OTA_SD_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || defined(OTA_QSPI_BOOTLOADER_UPDATE) + #if defined(OTA_RAK15002_SD_CACHE) && (defined(OTA_SD_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || defined(OTA_QSPI_BOOTLOADER_UPDATE)) #error "RAK15002 cache profile has no qualified bootloader-update handoff" #endif #ifndef OTA_SD_CS_PIN @@ -20,14 +33,26 @@ #endif static_assert(OTA_SD_CS_PIN == 26 && PIN_SPI_MISO == 29 && PIN_SPI_SCK == 3 && PIN_SPI_MOSI == 30, "RAK15002 exact WisBlock SPI pin map changed; requalify the profile"); +#if defined(OTA_RAK15002_SD_STORE) + static_assert(WB_IO6 == 10, "RAK15002 card detect must use the mapped WisBlock IO6"); +#endif #elif !defined(OTA_SD_CS_PIN) #define OTA_SD_CS_PIN PIN_SPI1_NSS #endif namespace mesh { namespace ota { +#if defined(OTA_RAK15002_SD_STORE) +inline bool ota_rak15002_sd_card_present() { + // RAK15002 CD is active-low WisBlock IO6 (Arduino 10 / P0.10), not pin 6. + // GPIO-only detection must precede any SD or NOR command on shared CS26. + pinMode(WB_IO6, INPUT_PULLUP); + return digitalRead(WB_IO6) == LOW; +} +#endif + inline SPIClass& ota_sd_spi_bus() { -#if defined(OTA_RAK15002_SD_CACHE) +#if defined(OTA_RAK15002_SD_CACHE) || defined(OTA_RAK15002_SD_STORE) // The RAK radio reassigns global SPI/SPIM3 to its private pins. Never use // that object for the external WisBlock bus, or reconfigure it on SD mount. static SPIClass sd_spi(NRF_SPIM2, 29, 3, 30); diff --git a/src/helpers/ota/OtaSdTypesNrf52.h b/src/helpers/ota/OtaSdTypesNrf52.h new file mode 100644 index 000000000..496385684 --- /dev/null +++ b/src/helpers/ota/OtaSdTypesNrf52.h @@ -0,0 +1,30 @@ +#pragma once + +#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) + +// Keep SdFat implementation headers out of application headers: its global +// File typedef conflicts with Adafruit LittleFS's File. Only the two SD +// implementation translation units need complete volume/file definitions. +#if defined(OTA_RAK15002_SD_STORE) +class SdFat32; +class FatFile; +#else +class SdFs; +class FsFile; +#endif + +namespace mesh { +namespace ota { + +#if defined(OTA_RAK15002_SD_STORE) +using OtaSdFs = SdFat32; +using OtaSdFile = FatFile; +#else +using OtaSdFs = SdFs; +using OtaSdFile = FsFile; +#endif + +} // namespace ota +} // namespace mesh + +#endif diff --git a/src/helpers/ota/OtaStoreAdaptiveNrf52.h b/src/helpers/ota/OtaStoreAdaptiveNrf52.h index 2cfd7079a..00e5f5a1e 100644 --- a/src/helpers/ota/OtaStoreAdaptiveNrf52.h +++ b/src/helpers/ota/OtaStoreAdaptiveNrf52.h @@ -8,7 +8,11 @@ #include "OtaStoreFlashNrf52.h" #include "OtaStoreQspiNrf52.h" #include "OtaRakStoragePolicy.h" -#if defined(RAK4631_COMBINED_ETHERNET) +#if defined(OTA_RAK15002_SD_STORE) + #include "OtaStoreSdNrf52.h" + #include "OtaSdSpiBusNrf52.h" +#endif +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) #include "../nrf52/Rak4631SharedSpi.h" #endif #include @@ -17,14 +21,21 @@ namespace mesh { namespace ota { // One RAK application image supports the board's internal OTA store and its -// explicitly matched external NOR. Application placement follows the detected +// explicitly matched external NOR or RAK15002 SD. Application placement follows the detected // hardware. Bootloader packages always use internal flash; select the backend // before begin(), never move a partially fetched container between stores. class OtaStoreAdaptiveNrf52 : public OtaStore { - enum Mode : uint8_t { UNSELECTED, INTERNAL_MODE, QSPI_MODE, UNSAFE }; + enum Mode : uint8_t { UNSELECTED, INTERNAL_MODE, QSPI_MODE, SD_MODE, UNSAFE }; mutable Mode _mode = UNSELECTED; mutable const char* _reason = "not probed"; bool _bootloader_store = false; +#if defined(OTA_RAK15002_SD_STORE) + // The SD cache retains a stable owner even while a bootloader package uses + // internal flash. Card discovery is deferred until an explicit main-loop + // prepare; status/capability getters must not clock an optional empty slot. + mutable OtaStoreSdNrf52 _sd; + bool _prepared = false; +#endif // Only one backend is usable for a given board/bootloader pairing. Sharing // their two flash-page buffers saves 8 KiB of scarce nRF52840 runtime RAM. union StoreStorage { @@ -46,6 +57,9 @@ class OtaStoreAdaptiveNrf52 : public OtaStore { bool selectBootloader() { if (!ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps())) return false; +#if defined(OTA_RAK15002_SD_STORE) + if (_sd.staged_size() != 0u) return false; +#endif if (_mode != INTERNAL_MODE) { if (_mode == QSPI_MODE) _storage.external.~OtaStoreQspiNrf52(); activateInternal(); @@ -61,18 +75,43 @@ class OtaStoreAdaptiveNrf52 : public OtaStore { _mode = UNSELECTED; _bootloader_store = false; } +#if defined(OTA_RAK15002_SD_STORE) + selectStorage(); +#else select(); +#endif } void select() const { if (_mode != UNSELECTED) return; -#if defined(RAK4631_COMBINED_ETHERNET) +#if defined(OTA_RAK15002_SD_STORE) + if (!_prepared) return; +#endif +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) const bool ethernet = rak4631_ethernet_owns_spi(); - const uint8_t detected = ethernet ? 0u : OtaStoreQspiNrf52::autoDetect(); #else - const uint8_t detected = OtaStoreQspiNrf52::autoDetect(); + const bool ethernet = false; #endif const OtaBlCaps caps = ota_bootloader_app_caps(); +#if defined(OTA_RAK15002_SD_STORE) + // Card detect is GPIO-only. If a card is fitted, never issue JEDEC clocks + // to the same CS as a fallback, even when its media/loader is unsuitable. + const bool sd_present = !ethernet && ota_rak15002_sd_card_present(); + if (sd_present && caps.optional_sd_app_storage == OTA_BL_STORAGE_SD) { + OtaBootloaderIdentity sd_identity; + if (ota_installed_bootloader_identity(sd_identity) && sd_identity.crc_ok && + (strcmp(sd_identity.device_name, "4631_DFU") == 0 || + strcmp(sd_identity.device_name, "4631_AUTO_DFU") == 0) && + _sd.probeMedia()) { + _mode = SD_MODE; + _reason = "RAK15002 SD"; + return; + } + } + const uint8_t detected = (ethernet || sd_present) ? 0u : OtaStoreQspiNrf52::autoDetect(); +#else + const uint8_t detected = ethernet ? 0u : OtaStoreQspiNrf52::autoDetect(); +#endif if (detected == 3u) { _mode = UNSAFE; _reason = "both external NOR types detected"; @@ -100,7 +139,7 @@ class OtaStoreAdaptiveNrf52 : public OtaStore { caps.optional_app_storage == (OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_HEADER_W25)); if (choice == RakStorageChoice::Internal) { activateInternal(); -#if defined(RAK4631_COMBINED_ETHERNET) +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) if (ethernet) { _reason = "Ethernet active; internal application deltas only"; return; @@ -108,6 +147,13 @@ class OtaStoreAdaptiveNrf52 : public OtaStore { #endif _reason = detected == 0u ? "no external NOR" : "external NOR fitted; internal bootloader"; +#if defined(OTA_RAK15002_SD_STORE) + if (sd_present) { + _reason = caps.optional_sd_app_storage == OTA_BL_STORAGE_SD + ? "SD unavailable; internal application deltas only" + : "SD needs newer OTAFIX; internal application deltas only"; + } +#endif return; } if (choice == RakStorageChoice::Qspi) { @@ -117,7 +163,7 @@ class OtaStoreAdaptiveNrf52 : public OtaStore { return; } _mode = UNSAFE; -#if defined(RAK4631_COMBINED_ETHERNET) +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) if (ethernet) { _reason = "Ethernet active; bootloader lacks internal OTA"; return; @@ -134,27 +180,55 @@ class OtaStoreAdaptiveNrf52 : public OtaStore { OtaStore* active() { select(); -#if defined(RAK4631_COMBINED_ETHERNET) - if (_mode == QSPI_MODE && rak4631_ethernet_owns_spi()) return nullptr; +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) + if ((_mode == QSPI_MODE || _mode == SD_MODE) && rak4631_ethernet_owns_spi()) return nullptr; +#endif +#if defined(OTA_RAK15002_SD_STORE) + if (_mode == SD_MODE) return &_sd; #endif return _mode == QSPI_MODE ? static_cast(&_storage.external) : _mode == INTERNAL_MODE ? static_cast(&_storage.internal) : nullptr; } const OtaStore* active() const { select(); -#if defined(RAK4631_COMBINED_ETHERNET) - if (_mode == QSPI_MODE && rak4631_ethernet_owns_spi()) return nullptr; +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) + if ((_mode == QSPI_MODE || _mode == SD_MODE) && rak4631_ethernet_owns_spi()) return nullptr; +#endif +#if defined(OTA_RAK15002_SD_STORE) + if (_mode == SD_MODE) return &_sd; #endif return _mode == QSPI_MODE ? static_cast(&_storage.external) : _mode == INTERNAL_MODE ? static_cast(&_storage.internal) : nullptr; } public: -#if defined(RAK4631_COMBINED_ETHERNET) + bool selectStorage() { +#if defined(OTA_RAK15002_SD_STORE) + // A cancelled/cleared privileged fetch may return to its application + // backend without tearing down the independent SD cache owner. Never + // reselect while internal bootloader staging still contains data. + if (_bootloader_store && _mode == INTERNAL_MODE && + _storage.internal.staged_size() == 0u) { + _storage.internal.~OtaStoreFlashNrf52(); + _mode = UNSELECTED; + _bootloader_store = false; + } + _prepared = true; +#endif + select(); + return _mode != UNSELECTED && _mode != UNSAFE; + } +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) // Only the main-loop handoff may reset the selection, after OtaContext has // proved that no receive/apply session is active. Never discard staged data // as a side effect of changing a network setting. bool resetSelection() { +#if defined(OTA_RAK15002_SD_STORE) + if (_sd.staged_size() != 0u) return false; + if ((_mode == QSPI_MODE && _storage.external.staged_size() != 0u) || + (_mode == INTERNAL_MODE && _storage.internal.staged_size() != 0u) || + !_sd.releaseBus()) return false; +#endif if (_mode == QSPI_MODE) { if (_storage.external.staged_size() != 0u) return false; _storage.external.~OtaStoreQspiNrf52(); @@ -162,6 +236,9 @@ public: if (_storage.internal.staged_size() != 0u) return false; _storage.internal.~OtaStoreFlashNrf52(); } +#if defined(OTA_RAK15002_SD_STORE) + _prepared = false; +#endif _mode = UNSELECTED; _reason = "not probed"; _bootloader_store = false; @@ -172,17 +249,25 @@ public: if (_mode == QSPI_MODE) _storage.external.~OtaStoreQspiNrf52(); else if (_mode == INTERNAL_MODE) _storage.internal.~OtaStoreFlashNrf52(); } - bool usesExternal() const { select(); return _mode == QSPI_MODE; } + bool usesExternal() const { select(); return _mode == QSPI_MODE || _mode == SD_MODE; } + bool usesQspi() const { select(); return _mode == QSPI_MODE; } + bool usesSd() const { select(); return _mode == SD_MODE; } bool usesInternal() const { select(); return _mode == INTERNAL_MODE; } const char* selectionReason() const { select(); return _reason; } OtaStoreFlashNrf52& internalStore() { select(); return _storage.internal; } OtaStoreQspiNrf52& externalStore() { select(); return _storage.external; } - uint32_t qspiCapacity() const { return usesExternal() ? _storage.external.capacity() : 0u; } - uint32_t jedec_id() const { return usesExternal() ? _storage.external.jedec_id() : 0u; } - uint8_t status1() const { return usesExternal() ? _storage.external.status1() : 0xFFu; } - const char* last_stage() const { return usesExternal() ? _storage.external.last_stage() : "inactive"; } +#if defined(OTA_RAK15002_SD_STORE) + OtaStoreSdNrf52& sdStore() { return _sd; } +#endif + uint32_t qspiCapacity() const { return usesQspi() ? _storage.external.capacity() : 0u; } + uint32_t jedec_id() const { return usesQspi() ? _storage.external.jedec_id() : 0u; } + uint8_t status1() const { return usesQspi() ? _storage.external.status1() : 0xFFu; } + const char* last_stage() const { return usesQspi() ? _storage.external.last_stage() : "inactive"; } const char* last_error() const { - return usesExternal() ? _storage.external.last_error() : selectionReason(); +#if defined(OTA_RAK15002_SD_STORE) + if (usesSd()) return _sd.last_error(); +#endif + return usesQspi() ? _storage.external.last_error() : selectionReason(); } bool begin(uint32_t n) override { OtaStore* s = active(); return s && s->begin(n); } diff --git a/src/helpers/ota/OtaStoreQspiNrf52.cpp b/src/helpers/ota/OtaStoreQspiNrf52.cpp index 43489c868..16999920e 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.cpp +++ b/src/helpers/ota/OtaStoreQspiNrf52.cpp @@ -13,9 +13,12 @@ #include #endif #include -#if defined(RAK4631_COMBINED_ETHERNET) +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) #include "../nrf52/Rak4631SharedSpi.h" #endif +#if defined(OTA_RAK15002_SD_STORE) + #include "OtaSdSpiBusNrf52.h" +#endif namespace mesh { namespace ota { @@ -327,12 +330,20 @@ static uint32_t probe_nor(uint8_t cs) { #if defined(OTA_RAK_AUTO_STORE) uint8_t OtaStoreQspiNrf52::autoDetect() { +#if defined(OTA_RAK15002_SD_STORE) + // SD and the RAK15001 use the same CS26. Even an unmounted/unsupported + // inserted card must never see the NOR wake or JEDEC command sequence. + if (ota_rak15002_sd_card_present()) return 0u; +#endif #if defined(RAK4631_COMBINED_ETHERNET) // Do not even bit-bang a JEDEC probe while the Ethernet task owns SPI. // Leave the cached physical discovery unchanged for the next idle handoff. if (rak4631_ethernet_owns_spi()) return 0u; #endif if (auto_detection != 0xFFu) return auto_detection; +#if defined(OTA_RAK15002_SD_STORE) + if (!rak4631_try_acquire_spi(RakSharedSpiOwner::Nor)) return 0u; +#endif const uint8_t w25_cs = arduino_to_physical(31); nrf_gpio_pin_set(w25_cs); @@ -382,6 +393,9 @@ uint8_t OtaStoreQspiNrf52::autoDetect() { auto_cs_pin = w25_cs; auto_jedec_id = 0xEF4015UL; } +#if defined(OTA_RAK15002_SD_STORE) + rak4631_release_spi(RakSharedSpiOwner::Nor); +#endif return auto_detection; } @@ -487,6 +501,12 @@ bool OtaStoreQspiNrf52::waitMemoryReady(uint32_t timeout_ms) { } bool OtaStoreQspiNrf52::ensureFlash() { +#if defined(OTA_RAK15002_SD_STORE) + if (ota_rak15002_sd_card_present() || !rak4631_try_acquire_spi(RakSharedSpiOwner::Nor)) { + fail("SD card present or WisBlock SPI owned by SD/Ethernet"); + return false; + } +#endif #if defined(RAK4631_COMBINED_ETHERNET) if (rak4631_ethernet_owns_spi()) { fail("Ethernet owns WisBlock SPI; external OTA unavailable"); @@ -627,8 +647,13 @@ bool OtaStoreQspiNrf52::ensureFlash() { } void OtaStoreQspiNrf52::releaseFlash() { +#if defined(OTA_RAK15002_SD_STORE) + // The failed-acquire path must not touch somebody else's live controller, + // chip select or supply. No callback may forcibly overwrite this claim. + if (rak4631_spi_owner() != RakSharedSpiOwner::Nor) return; +#endif bool send_power_down = _qspi_awake && !_memory_operation_pending; -#if defined(RAK4631_COMBINED_ETHERNET) +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) send_power_down = send_power_down && !rak4631_ethernet_owns_spi(); #endif if (send_power_down) { @@ -650,7 +675,7 @@ void OtaStoreQspiNrf52::releaseFlash() { // disabling the peripheral. Trigger it after every successful ENABLE, even // when wake or JEDEC identification failed before _qspi_ready was set. if (_qspi_active) { -#if defined(RAK4631_COMBINED_ETHERNET) +#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE) // Also release the QSPI pin assignments after a successful operation so // the Ethernet SPIM can safely acquire the same pads at an idle handoff. const bool disconnect_pins = true; @@ -699,6 +724,11 @@ void OtaStoreQspiNrf52::releaseFlash() { _shared_radio_bus_acquired = false; } #endif +#if defined(OTA_RAK15002_SD_STORE) + // An uncertain program/erase retains exclusive ownership until a later + // successful status poll proves the NOR idle; never hand its nets to SD. + if (!_memory_operation_pending) rak4631_release_spi(RakSharedSpiOwner::Nor); +#endif } bool OtaStoreQspiNrf52::dmaReadAligned(uint32_t address, uint32_t length) { diff --git a/src/helpers/ota/OtaStoreQspiNrf52.h b/src/helpers/ota/OtaStoreQspiNrf52.h index dfa739d9d..7dc70bf39 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.h +++ b/src/helpers/ota/OtaStoreQspiNrf52.h @@ -7,7 +7,9 @@ #endif #if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(ETHERNET_ENABLED) && \ - !defined(RAK4631_COMBINED_ETHERNET) + !defined(RAK4631_COMBINED_ETHERNET) && \ + !(defined(OTA_RAK15002_SD_STORE) && defined(NRF52_PLATFORM) && \ + defined(RAK_4631) && defined(OTA_RAK_AUTO_STORE)) #error "WisBlock SPI OTA staging cannot share the bus/chip-select with Ethernet" #endif @@ -17,7 +19,8 @@ #error "Combined RAK4631 Ethernet requires its adaptive OTA and shared-SPI ownership contract" #endif -#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(OTA_SD_STORE) +#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(OTA_SD_STORE) && \ + !(defined(OTA_RAK15002_SD_STORE) && defined(RAK_4631) && defined(OTA_RAK_AUTO_STORE)) #error "WisBlock SPI OTA staging cannot share the bus/chip-select with SD staging" #endif diff --git a/src/helpers/ota/OtaStoreSdNrf52.cpp b/src/helpers/ota/OtaStoreSdNrf52.cpp index 2805af410..aae4092e2 100644 --- a/src/helpers/ota/OtaStoreSdNrf52.cpp +++ b/src/helpers/ota/OtaStoreSdNrf52.cpp @@ -22,6 +22,14 @@ #define OTA_SD_SCK_MHZ 8 #endif +#if defined(OTA_RAK15002_SD_STORE) +// The mounted FAT/exFAT volume and both retained file handles (staging and +// archive) are lazy heap objects, not linked globals. Bind their total plus +// allocator metadata to the runtime RAM policy's separate 2 KiB allowance. +static_assert(sizeof(mesh::ota::OtaSdFs) + 2u * sizeof(mesh::ota::OtaSdFile) + 3u * 32u <= 2048u, + "RAK15002 FAT volume/file allocations exceed the RAM allowance"); +#endif + namespace mesh { namespace ota { @@ -35,6 +43,7 @@ static void clear_sd_auth_record() { __DSB(); } +#if !defined(OTA_SEEDER_ONLY) static bool publish_sd_auth_record(const uint8_t record[MOTA_SD_AUTH_LEN]) { volatile uint8_t* const retained = reinterpret_cast(MOTA_SD_AUTH_ADDR); @@ -52,13 +61,17 @@ static bool publish_sd_auth_record(const uint8_t record[MOTA_SD_AUTH_LEN]) { if (retained[i] != record[i]) return false; return true; } +#endif } // namespace const char* const OtaStoreSdNrf52::PATH = "/meshcore-ota.mota"; OtaStoreSdNrf52::OtaStoreSdNrf52() - : _sd(new (std::nothrow) SdFs()), _file(new (std::nothrow) FsFile()) { +#if !defined(OTA_RAK15002_SD_STORE) + : _sd(new (std::nothrow) OtaSdFs()), _file(new (std::nothrow) OtaSdFile()) +#endif +{ // If an earlier reset did not enter OTAFIX, stale authorization must not arm a // later unrelated reboot. OTAFIX runs before application constructors and // consumes a valid record on the intended apply boot. @@ -66,8 +79,59 @@ OtaStoreSdNrf52::OtaStoreSdNrf52() } OtaStoreSdNrf52::~OtaStoreSdNrf52() { - if (_file) { _file->close(); delete _file; } - if (_sd) { _sd->end(); delete _sd; } + resetStoreState(); + if (_file) delete _file; + if (_sd) delete _sd; + releaseBusClaim(); +} + +bool OtaStoreSdNrf52::acquireBus() { +#if defined(OTA_RAK15002_SD_STORE) + if (!ota_rak15002_sd_card_present()) { fail("SD card absent"); return false; } + if (!rak4631_try_acquire_spi(RakSharedSpiOwner::Sd)) { + fail("WisBlock SPI owned by Ethernet or NOR"); return false; + } + _bus_owned = true; + if (!_sd) _sd = new (std::nothrow) OtaSdFs(); + if (!_file) _file = new (std::nothrow) OtaSdFile(); + if (!_sd || !_file) { + fail("SD allocation failed"); releaseBusClaim(); return false; + } +#endif + return true; +} + +bool OtaStoreSdNrf52::busAccessAllowed() const { +#if defined(OTA_RAK15002_SD_STORE) + return _bus_owned && rak4631_spi_owner() == RakSharedSpiOwner::Sd + && ota_rak15002_sd_card_present(); +#else + return true; +#endif +} + +void OtaStoreSdNrf52::releaseBusClaim() { +#if defined(OTA_RAK15002_SD_STORE) + if (!_bus_owned) return; + if (_bus_started) { + ota_sd_spi_bus().end(); + // Leave the powered, unswitched card deselected, and stop driving the + // shared clock/data pads before another peripheral claims them. + digitalWrite(OTA_SD_CS_PIN, HIGH); + pinMode(OTA_SD_CS_PIN, OUTPUT); + pinMode(3, INPUT); pinMode(29, INPUT); pinMode(30, INPUT); + _bus_started = false; + } + rak4631_release_spi(RakSharedSpiOwner::Sd); + _bus_owned = false; +#endif +} + +bool OtaStoreSdNrf52::releaseBus() { + if (_total) { fail("SD staged update is pinned"); return false; } + resetStoreState(); + releaseBusClaim(); + return true; } void OtaStoreSdNrf52::fail(const char* message) { @@ -80,31 +144,48 @@ void OtaStoreSdNrf52::resetStoreState() { _first_sector = 0; _allocated_sectors = 0; _planned_bootloader = false; - if (_file && *_file) _file->close(); - if (_sd) _sd->end(); +#if defined(OTA_RAK15002_SD_STORE) + // Ownership persists if a card is physically removed. Cleanup still owns + // the controller and can release it, but no other device may steal it. + const bool access = _bus_owned && rak4631_spi_owner() == RakSharedSpiOwner::Sd; +#else + const bool access = true; +#endif + if (access && _file && *_file) _file->close(); + if (access && _sd) _sd->end(); _mounted = false; } bool OtaStoreSdNrf52::beginCardOnly() { _error[0] = 0; + if (!acquireBus()) return false; resetStoreState(); ota_sd_prepare_chip_select(); +#if defined(OTA_RAK15002_SD_STORE) + _bus_started = true; +#endif if (!_sd || !_file || !_sd->cardBegin(SdSpiConfig(OTA_SD_CS_PIN, DEDICATED_SPI, SD_SCK_MHZ(OTA_SD_SCK_MHZ), &ota_sd_spi_bus()))) { fail("SD card init failed"); + resetStoreState(); releaseBusClaim(); return false; } return true; } bool OtaStoreSdNrf52::mount() { - if (_mounted) return true; + if (_mounted) return busAccessAllowed(); _error[0] = 0; + if (!acquireBus()) return false; ota_sd_prepare_chip_select(); +#if defined(OTA_RAK15002_SD_STORE) + _bus_started = true; +#endif if (!_sd || !_file || !_sd->begin(SdSpiConfig(OTA_SD_CS_PIN, DEDICATED_SPI, SD_SCK_MHZ(OTA_SD_SCK_MHZ), &ota_sd_spi_bus()))) { fail("SD mount failed"); + resetStoreState(); releaseBusClaim(); return false; } _mounted = true; @@ -119,12 +200,14 @@ bool OtaStoreSdNrf52::formatCard(MainBoard& board) { if (!_sd->format()) { fail("SD format failed"); _sd->end(); + releaseBusClaim(); return false; } board.serviceWatchdog(); if (!_sd->volumeBegin()) { fail("SD remount after format failed"); _sd->end(); + releaseBusClaim(); return false; } @@ -142,6 +225,7 @@ bool OtaStoreSdNrf52::eraseCard(MainBoard& board) { if (!card || sector_count == 0) { fail("SD sector count failed"); _sd->end(); + releaseBusClaim(); return false; } @@ -159,6 +243,7 @@ bool OtaStoreSdNrf52::eraseCard(MainBoard& board) { if (!card->erase(first_sector, last_sector)) { fail("SD erase failed"); _sd->end(); + releaseBusClaim(); return false; } first_sector += count; @@ -168,9 +253,11 @@ bool OtaStoreSdNrf52::eraseCard(MainBoard& board) { if (!card->syncDevice()) { fail("SD erase sync failed"); _sd->end(); + releaseBusClaim(); return false; } _sd->end(); + releaseBusClaim(); return true; } @@ -257,8 +344,8 @@ static bool append_sd_list_entry(SdListState& state, const char* path, uint64_t return true; } -static bool scan_sd_files(FsFile& dir, SdListState& state) { - FsFile file; +static bool scan_sd_files(OtaSdFile& dir, SdListState& state) { + OtaSdFile file; while (file.openNext(&dir, O_RDONLY)) { state.board.serviceWatchdog(); const size_t parent_len = state.path_len; @@ -309,7 +396,7 @@ bool OtaStoreSdNrf52::listFiles(MainBoard& board, uint16_t page, _error[0] = 0; if (!mount()) return false; - FsFile root = _sd->open("/", O_RDONLY); + OtaSdFile root = _sd->open("/", O_RDONLY); if (!root || !root.isDir()) { fail("SD root directory open failed"); return false; } SdListState state(board, (uint32_t)(page - 1) * SD_LIST_PAGE_SIZE); bool scanned = ensure_list_path(state, 2) && scan_sd_files(root, state); @@ -331,7 +418,37 @@ bool OtaStoreSdNrf52::listFiles(MainBoard& board, uint16_t page, return true; } +uint32_t OtaStoreSdNrf52::capacity() const { + if (!busAccessAllowed() || !_mounted || !_sd || !_sd->card()) return 0; + uint64_t bytes = (uint64_t)_sd->card()->sectorCount() * MOTA_SD_AUTH_SECTOR_SIZE; + return bytes > UINT32_MAX ? UINT32_MAX : (uint32_t)bytes; +} + +#if defined(OTA_SEEDER_ONLY) +// The source-only context uses OtaStoreRam<1> for install fetches and this +// object only owns the FAT volume shared with OtaCacheSdNrf52. Its archive +// has its own file/read/write methods. Do not retain unreachable installer +// vtable code (or permit accidental staging/approval) in a Companion image. +bool OtaStoreSdNrf52::plan_layout(bool, uint32_t, uint32_t, uint32_t, bool) { + fail("source-only SD archive cannot stage/apply"); return false; +} +bool OtaStoreSdNrf52::begin(uint32_t) { + fail("source-only SD archive cannot stage/apply"); return false; +} +bool OtaStoreSdNrf52::set_meta_size(uint32_t) { return false; } +bool OtaStoreSdNrf52::write(uint32_t, const uint8_t*, uint32_t) { return false; } +bool OtaStoreSdNrf52::read(uint32_t, uint8_t*, uint32_t) const { return false; } +bool OtaStoreSdNrf52::finalize() { return false; } +void OtaStoreSdNrf52::checkpoint() {} +bool OtaStoreSdNrf52::reopen() { return false; } +bool OtaStoreSdNrf52::discard() { return false; } +void OtaStoreSdNrf52::clear() {} +bool OtaStoreSdNrf52::approve_for_bootloader(const uint8_t[32], const uint8_t[32]) { + fail("source-only SD archive cannot stage/apply"); return false; +} +#else bool OtaStoreSdNrf52::locate_file() { + if (!busAccessAllowed()) return false; Sector_t first = 0, last = 0; if (!_file || !_file->contiguousRange(&first, &last) || last < first) { fail("OTA file is not contiguous"); @@ -418,7 +535,7 @@ bool OtaStoreSdNrf52::set_meta_size(uint32_t meta_bytes) { } bool OtaStoreSdNrf52::write(uint32_t offset, const uint8_t* data, uint32_t len) { - if (!_file || !*_file || (uint64_t)offset + len > _total || !_file->seekSet(offset) || + if (!busAccessAllowed() || !_file || !*_file || (uint64_t)offset + len > _total || !_file->seekSet(offset) || _file->write(data, len) != len) { fail("SD OTA write failed"); return false; @@ -427,20 +544,15 @@ bool OtaStoreSdNrf52::write(uint32_t offset, const uint8_t* data, uint32_t len) } bool OtaStoreSdNrf52::read(uint32_t offset, uint8_t* buf, uint32_t len) const { - if (!_file || !*_file || (uint64_t)offset + len > _total || !_file->seekSet(offset) || + if (!busAccessAllowed() || !_file || !*_file || (uint64_t)offset + len > _total || !_file->seekSet(offset) || _file->read(buf, len) != (int)len) { return false; } return true; } -uint32_t OtaStoreSdNrf52::capacity() const { - if (!_mounted || !_sd || !_sd->card()) return 0; - uint64_t bytes = (uint64_t)_sd->card()->sectorCount() * MOTA_SD_AUTH_SECTOR_SIZE; - return bytes > UINT32_MAX ? UINT32_MAX : (uint32_t)bytes; -} - bool OtaStoreSdNrf52::finalize() { + if (!busAccessAllowed()) return false; SdCard* const card = _sd ? _sd->card() : nullptr; if (!_file || !*_file || !card || !_file->sync() || !card->syncDevice()) { fail("SD OTA sync failed"); @@ -450,7 +562,7 @@ bool OtaStoreSdNrf52::finalize() { } void OtaStoreSdNrf52::checkpoint() { - if (_file && *_file) { + if (busAccessAllowed() && _file && *_file) { _file->sync(); if (_sd->card()) _sd->card()->syncDevice(); } @@ -604,6 +716,7 @@ bool OtaStoreSdNrf52::approve_for_bootloader( } return true; } +#endif // OTA_SEEDER_ONLY } // namespace ota } // namespace mesh diff --git a/src/helpers/ota/OtaStoreSdNrf52.h b/src/helpers/ota/OtaStoreSdNrf52.h index fcfc1bcfa..44b9e98e5 100644 --- a/src/helpers/ota/OtaStoreSdNrf52.h +++ b/src/helpers/ota/OtaStoreSdNrf52.h @@ -3,9 +3,7 @@ #if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) #include "OtaStore.h" - -class SdFs; -class FsFile; +#include "OtaSdTypesNrf52.h" namespace mesh { @@ -55,6 +53,9 @@ public: // Mount-only probe for the MeshTower selector. Never opens/removes staging // files or changes their live session state. bool probeMedia() { return mount() && capacity() != 0u; } + // Caller first detaches the archive and proves no queued/pinned SD source + // remains. Never unmount an installable or partially received container. + bool releaseBus(); const char* last_error() const { return _error; } private: @@ -62,14 +63,21 @@ private: static const char* const PATH; bool mount(); + bool acquireBus(); + bool busAccessAllowed() const; + void releaseBusClaim(); bool beginCardOnly(); bool locate_file(); void resetStoreState(); void fail(const char* message); - SdFs* _sd = nullptr; - mutable FsFile* _file = nullptr; + OtaSdFs* _sd = nullptr; + mutable OtaSdFile* _file = nullptr; bool _mounted = false; +#if defined(OTA_RAK15002_SD_STORE) + bool _bus_owned = false; + bool _bus_started = false; +#endif bool _planned_bootloader = false; uint32_t _total = 0; uint32_t _first_sector = 0; diff --git a/src/helpers/ota/OtaTargets.h b/src/helpers/ota/OtaTargets.h index c1f4603bc..17c74511b 100644 --- a/src/helpers/ota/OtaTargets.h +++ b/src/helpers/ota/OtaTargets.h @@ -3,7 +3,7 @@ #include // AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand. -// 659 OTA-capable build targets. Maps target_id (= sha2-256:4 of the target name, LE uint32) +// 683 OTA-capable build targets. Maps target_id (= sha2-256:4 of the target name, LE uint32) // to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT // transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes. // Size-constrained receivers can set OTA_TARGET_NAME_TABLE=0. They still match targets by ID; @@ -19,6 +19,9 @@ #if OTA_TARGET_NAME_TABLE && OTA_TARGET_NAME_FRONT_CODED #include "tinf/tinf.h" + #if defined(OTA_RAK15002_SD_STORE) && defined(COMPANION_RADIO_FULL) + #include + #endif #endif namespace mesh { namespace ota { @@ -47,68 +50,71 @@ inline const char* ota_target_env_name(uint32_t target_id, char* buffer, size_t 0x2f9f8e7d, 0xc9dc5d8c, 0x34b0e5f3, 0xb4d0ad97, 0xcfa0e233, 0x17d5e1ff, 0xc99313c6, 0xffb3854f, 0x21a73973, 0x3067f656, 0xfdb25057, 0x836e5931, 0x0f1767cd, 0x57172fcc, 0xa529b4f5, 0x85a8c944, 0x3c60586d, 0x0a9dbbf0, 0xb519a89f, 0xa3e8d8ce, 0xb16009a0, 0xdc780e80, 0x28b8007c, 0x4aa63180, - 0x6fddcbf1, 0xf4fb5484, 0x2e5384e2, 0xb62a1e75, 0x9e64845d, 0x19232e8b, 0xfaf164f4, 0x38b948b4, - 0x462ddb9b, 0xed21ca4f, 0x9f5f39a7, 0x081d2219, 0x7c498bc4, 0xc283ad60, 0x4fbdc1e1, 0xc797fd73, - 0xab9871c8, 0x2075687c, 0x5d726067, 0x9969bbc9, 0x804f7a3a, 0x0823a21a, 0x22f900de, 0xaca0e153, - 0x516c9c13, 0xcb1c2e65, 0xd1b29b18, 0x644bb68d, 0xd19a9759, 0xde40f56f, 0x6dc15ab3, 0xc59d294e, - 0xceeddfaf, 0x21519537, 0x382bb181, 0xad1bcd63, 0x5a2980e6, 0x90581c83, 0x90a6a854, 0x356df17c, - 0x99a084fc, 0x64c9dfc9, 0x2a2ffc92, 0x2c13b8e9, 0x1ee7d867, 0xb016979a, 0xb49b6b55, 0x01d8f124, - 0x86fb85f0, 0xd33b0986, 0x1a4d0096, 0xa8f5940f, 0x1cf5e14c, 0x2707d3d1, 0x86ac3b1b, 0x7af848da, - 0xd522a14f, 0x76657d76, 0x21ee03bd, 0x5977d941, 0xe2a1907b, 0xbb0ca8db, 0xf75feb3e, 0xb0a1e71f, - 0xbc9cdc70, 0x19e31fc6, 0xa01b330e, 0x36c78d86, 0xdf722326, 0xfa901c87, 0xeebe3469, 0xb0f5aff6, - 0x5729ad07, 0xaa2aef42, 0xb4cb035a, 0xbb429dde, 0x883e3edc, 0x4a9d1c6c, 0xa0965a6b, 0x59ea25fa, - 0x1758bdb9, 0x1313f16d, 0x0372b0f1, 0xa2fb4192, 0xdd39457b, 0x7964461c, 0x1d1c0d92, 0x5fc52bcb, - 0x0dacb683, 0xe792a051, 0x2d5ea842, 0x30c59b1d, 0xab74db9e, 0xeed78ce4, 0x7778b7e1, 0x0b6847ed, - 0xab3e1ad3, 0x04a8b763, 0xc456437e, 0xb3ecafca, 0xf3afcde6, 0x87513d56, 0xca812b41, 0x18594231, - 0x188f3ac1, 0x402055ec, 0x12393f1f, 0x16fbab07, 0xed3ef5b8, 0x8ea86381, 0x09f57289, 0x148aeaab, - 0xcbb2bf38, 0x6e9f7fec, 0x9a340cd3, 0x6a25d024, 0xc5a2cb41, 0xc4a11710, 0x753947a4, 0xe6fa98b4, - 0xf8c27cf4, 0xa139c6ae, 0x90e227d9, 0x89e8d957, 0x812f5e52, 0x61f961c5, 0x1ddd15c8, 0x49d67d0d, - 0xbc003322, 0x835b70c0, 0xd7460575, 0x36acced5, 0x33e2c171, 0x5be91148, 0xa165ae99, 0x04c5c2b7, - 0x6c1ca659, 0xa295fb04, 0x61c405ca, 0xe893fe91, 0x2b599d31, 0xf1442d25, 0x15082ede, 0x6ff350d1, - 0xe03d0cd9, 0x82e1eb2c, 0x0ee19b12, 0x6c67ed89, 0xd1c05d0d, 0xb90150b0, 0x7285bdb3, 0x77cadc99, - 0xc177e6cd, 0xf7a38993, 0x83762f33, 0x86cadda9, 0x7e49da49, 0x305d6f92, 0xbc6b870b, 0x78845fdd, - 0x6d35f434, 0xa52d76a8, 0xd8de9639, 0x8ab5c51b, 0x80097676, 0xffe70df5, 0x3da80417, 0x47ca3c13, - 0x28b499c7, 0xf1ab4c28, 0xe5331f02, 0xfba7177b, 0xde7dc472, 0x3625bb80, 0x69df7765, 0x988cc4e1, - 0xe9fbc9be, 0x40818863, 0x65deac33, 0xd86cf67a, 0xed73ac03, 0xab0d550a, 0x1bd2a266, 0x3655c748, - 0x9a5ae8fc, 0x1e9eb97a, 0x139536f4, 0x0716c724, 0x8de23fa7, 0xb92ec96b, 0x420ebc63, 0x967ccaee, - 0x8021afa2, 0x5f7c7688, 0xbd04e6e7, 0x1aeac884, 0x07849209, 0x0c79b100, 0xbfb60e7c, 0x8a63de3a, - 0x96036a68, 0xb76a745c, 0x43023193, 0xa7d97db7, 0x468a133e, 0xb59ea2ed, 0xcb75495b, 0xbe372c6d, - 0xb762985a, 0x6215e9eb, 0x07907102, 0x3898ea56, 0x2fb7e218, 0x8c61d4c8, 0x4f722e70, 0x694eed25, - 0x85b8b9d7, 0xe8421e36, 0x82eaaf9e, 0x9f9caba4, 0xe4a2de74, 0x582c7826, 0xaeb637f1, 0x86813271, - 0x67b2ff84, 0xd0e2e616, 0x3f9edfe3, 0xc23f8082, 0x0e74ae16, 0x8f256e56, 0x6915c6e1, 0xf1407f39, - 0x1b4dc5ab, 0xe90a3181, 0x3b1b4237, 0x21c6d08d, 0x34302c14, 0x268d6fc1, 0xeabe3b40, 0x3c172d27, - 0xf7c5d584, 0x81960cc0, 0x504020ea, 0x9d8b60ae, 0xa2ce002f, 0xba4f47f3, 0x6f16479e, 0x1018e5d1, - 0xc0ab5040, 0xa2bf90bd, 0xe6bff8f9, 0xb9cd26cb, 0x9bc34bd5, 0x144504b0, 0xdbfe2675, 0xb00241d0, - 0x88e77fee, 0xae2d9caa, 0xa0043e00, 0x26bbf4ed, 0xdcc0f158, 0xe3320463, 0x852b928a, 0x0ec870e9, - 0x8f00c1f2, 0xcea95e20, 0xd45277ff, 0x2cad9a9d, 0x02a4a8c1, 0x4a709348, 0x35d960bd, 0x62977023, - 0x17a7ed8c, 0x07f5f5b5, 0xfd0d4b8f, 0x8d88f33c, 0xf5afb7dd, 0x3a0ea830, 0x6ebf28cb, 0x2d589527, - 0x77d0810c, 0x11b131e7, 0x54850763, 0x0fa61805, 0x8039574f, 0xa65555d6, 0x22b1804c, 0xa9f631b1, - 0x49d11eca, 0xd41f0638, 0xe3105eb8, 0xf8259880, 0x063f1f00, 0x70d71f92, 0xa8a79da5, 0xc0d3815b, - 0x966ac88e, 0x104b3f8b, 0x23b49348, 0xc42cc90d, 0xc24791c3, 0x15e11067, 0x2dbb2669, 0x90799c32, - 0x881a6a48, 0xf26af51c, 0x8174cbdc, 0x0bf0f061, 0x2509dd80, 0x56293b24, 0x8c81078c, 0xbd6a1c97, - 0xf4d98c12, 0xccac4283, 0xa68c09e9, 0x5f606274, 0x84923735, 0x2c0b8b05, 0xdba69401, 0xcb6555ee, - 0xd60daf25, 0x410bd800, 0x9aca380e, 0xd8641710, 0x4d0b1601, 0xab2ac09e, 0xa17fc4f7, 0xe4b2bd22, - 0x90c4eb3f, 0x19d9ce91, 0x121cc72f, 0xfa8510fa, 0xd34d8461, 0x98077550, 0x11628e29, 0x2ed959aa, - 0xf1e7cadb, 0xced7e127, 0x6925aaf0, 0x2fa509c1, 0xa52ba3c8, 0xaf0b81dd, 0x9f4d58ac, 0x7a55dec3, - 0x5e69738a, 0x48d3b5d4, 0x14e6965d, 0x76f3c984, 0x41e33a97, 0x29a0da19, 0x29bc38dc, 0x05f5ffae, - 0xc62da371, 0xf1d3c5a8, 0xc6d55752, 0x60683191, 0x8a7cb79b, 0x4fa4ae55, 0xd1d61a8b, 0x9cb0b232, - 0x51fa1a49, 0xdb73638b, 0xd904c2d5, 0x3a14aac7, 0x620569f4, 0x08483f6b, 0x0db48f35, 0x20532f6c, - 0x293ced32, 0xb38ebf22, 0x7c89c73d, 0x2d7c11e8, 0x5bb567cd, 0x61b32037, 0xad1cd53d, 0x0e18b360, - 0x3caf966e, 0xcb59ea4e, 0xe3a28284, 0xa8cb8e8b, 0x05747eb6, 0x75587f24, 0xf2128c81, 0x1b34e7b4, - 0x8e549407, 0x6ca142f4, 0x1deac038, 0x2a2f303b, 0x0ba4453d, 0xf8d1958f, 0x75e85141, 0xd62ed92e, - 0x7abaf0d5, 0x87edc08b, 0x3c49caf8, 0x58689ef9, 0x91878dd8, 0x72a93c45, 0x4dbf035e, 0xff4102d6, - 0x2f9a9185, 0x000fc519, 0x7b071fa0, 0x3e55e34d, 0x54606a4f, 0x16482630, 0xce0f07ad, 0x01c43d49, - 0x51fe3801, 0x857149e5, 0xd9d64938, 0xcb7da3f5, 0x9952fbe3, 0x3729cd74, 0xdc27c37c, 0x9b9f8775, - 0xb27514a3, 0xe9ce038c, 0xe295a7a4, 0x189660b3, 0x99907475, 0x2082078d, 0x24a94c81, 0x3750e80d, - 0xe4b49973, 0xe1372b52, 0x5a79bac1, 0x6f339514, 0x8eaa289e, 0x70e91bd9, 0x5a7e9c2d, 0x1211b151, - 0x0a32044b, 0xa13fe853, 0x79b9ff01, 0xc3decea2, 0x3c012018, 0x6afd6f75, 0x7517d400, 0xd7598668, - 0x61bdb52a, 0x413287d8, 0xe8cd2377, 0x3309bdaf, 0x7330afbb, 0x9df48ce6, 0xa5cb783a, 0x4b9f8283, - 0x311846c9, 0x8b6b29b7, 0x37928233, 0x88ac45da, 0x087f99c0, 0xdf41affe, 0x2afe6c63, 0xf2d61a1c, - 0x14b1b097, 0xec334f3f, 0x1da30414, 0xae8fe1c9, 0x67acd73f, 0xfb118ed8, 0xe4213729, 0x1298038d, - 0x2c47acff, 0x7d641646, 0x51efdbfe, 0xf2aada5b, 0xe59c9373, 0x236f605c, 0x93994fed, 0x66c47cfd, - 0x48061a01, 0x7ed7137d, 0xa01688a7, 0xa5f4a562, 0xef83222b, 0xc36ec867, 0xb932238e, 0x92f5bbea, - 0x26d91ab2, 0x708ac4dc, 0xdc51bf13, 0xc27f8304, 0x7190df40, 0x609d50b3, 0x5718c7ea, 0x37443a5b, - 0x603384a7, 0xce3ef40f, 0x475a2a6a, 0xf4175d2c, 0x3d831006, 0x556eb1bc, 0xb67370b3, 0x0c689b90, + 0x6fddcbf1, 0xf4fb5484, 0x2e5384e2, 0xb62a1e75, 0x9e64845d, 0x19232e8b, 0x10f41878, 0xfaf164f4, + 0x38b948b4, 0x462ddb9b, 0xed21ca4f, 0x9f5f39a7, 0x081d2219, 0x7c498bc4, 0xc283ad60, 0x4fbdc1e1, + 0xc797fd73, 0xab9871c8, 0x2075687c, 0x5d726067, 0x9969bbc9, 0x804f7a3a, 0x0823a21a, 0x22f900de, + 0xaca0e153, 0x516c9c13, 0xcb1c2e65, 0xd1b29b18, 0x644bb68d, 0xd19a9759, 0xde40f56f, 0x6dc15ab3, + 0xc59d294e, 0xceeddfaf, 0x21519537, 0x382bb181, 0xad1bcd63, 0x5a2980e6, 0x90581c83, 0x90a6a854, + 0x356df17c, 0x99a084fc, 0x64c9dfc9, 0x2a2ffc92, 0x2c13b8e9, 0x1ee7d867, 0xb016979a, 0xb49b6b55, + 0x01d8f124, 0x64140fd9, 0x86fb85f0, 0xd33b0986, 0x1a4d0096, 0xa8f5940f, 0x1cf5e14c, 0x2707d3d1, + 0x86ac3b1b, 0x7af848da, 0xd522a14f, 0x76657d76, 0x21ee03bd, 0x5977d941, 0xe2a1907b, 0xbb0ca8db, + 0xf75feb3e, 0xb0a1e71f, 0xbc9cdc70, 0x19e31fc6, 0xa01b330e, 0x36c78d86, 0xdf722326, 0xfa901c87, + 0xeebe3469, 0xb0f5aff6, 0x5729ad07, 0xaa2aef42, 0xb4cb035a, 0xbb429dde, 0x883e3edc, 0x4a9d1c6c, + 0xa0965a6b, 0x59ea25fa, 0x1758bdb9, 0x1313f16d, 0x0372b0f1, 0xa2fb4192, 0xdd39457b, 0x7964461c, + 0x1d1c0d92, 0x5fc52bcb, 0x0dacb683, 0xe792a051, 0x2d5ea842, 0x30c59b1d, 0xab74db9e, 0xeed78ce4, + 0x7778b7e1, 0x0b6847ed, 0xab3e1ad3, 0x04a8b763, 0xc456437e, 0xb3ecafca, 0xf3afcde6, 0x87513d56, + 0xca812b41, 0x18594231, 0x188f3ac1, 0x402055ec, 0x12393f1f, 0x16fbab07, 0xed3ef5b8, 0x8ea86381, + 0x09f57289, 0x148aeaab, 0xcbb2bf38, 0x6e9f7fec, 0x9a340cd3, 0x6a25d024, 0xc5a2cb41, 0xc4a11710, + 0x753947a4, 0xe6fa98b4, 0xf8c27cf4, 0xa139c6ae, 0x90e227d9, 0x89e8d957, 0x812f5e52, 0x61f961c5, + 0x1ddd15c8, 0x49d67d0d, 0xbc003322, 0x835b70c0, 0xd7460575, 0x36acced5, 0x33e2c171, 0x5be91148, + 0xa165ae99, 0x04c5c2b7, 0x6c1ca659, 0xa295fb04, 0x61c405ca, 0xe893fe91, 0x2b599d31, 0xf1442d25, + 0x15082ede, 0x6ff350d1, 0xe03d0cd9, 0x82e1eb2c, 0x0ee19b12, 0x6c67ed89, 0xd1c05d0d, 0xb90150b0, + 0x7285bdb3, 0x77cadc99, 0xc177e6cd, 0xf7a38993, 0x83762f33, 0x86cadda9, 0x7e49da49, 0x305d6f92, + 0xbc6b870b, 0x78845fdd, 0x6d35f434, 0xa52d76a8, 0xd8de9639, 0x8ab5c51b, 0x80097676, 0xffe70df5, + 0x3da80417, 0x47ca3c13, 0x28b499c7, 0xf1ab4c28, 0xe5331f02, 0xfba7177b, 0xde7dc472, 0x3625bb80, + 0x69df7765, 0x988cc4e1, 0xe9fbc9be, 0x40818863, 0x65deac33, 0xd86cf67a, 0xed73ac03, 0xab0d550a, + 0x1bd2a266, 0x3655c748, 0x9a5ae8fc, 0x1e9eb97a, 0x139536f4, 0x0716c724, 0x8de23fa7, 0xb92ec96b, + 0x420ebc63, 0x967ccaee, 0x8021afa2, 0x5f7c7688, 0xbd04e6e7, 0x1aeac884, 0x07849209, 0x0c79b100, + 0xbfb60e7c, 0x8a63de3a, 0x96036a68, 0xb76a745c, 0x43023193, 0xa7d97db7, 0x468a133e, 0xb59ea2ed, + 0xcb75495b, 0xbe372c6d, 0xb762985a, 0x6215e9eb, 0x07907102, 0x3898ea56, 0x2fb7e218, 0x8c61d4c8, + 0x4f722e70, 0x694eed25, 0x85b8b9d7, 0xe8421e36, 0x82eaaf9e, 0x9f9caba4, 0xe4a2de74, 0x582c7826, + 0xaeb637f1, 0x86813271, 0x67b2ff84, 0xd0e2e616, 0x3f9edfe3, 0xc23f8082, 0x0e74ae16, 0x8f256e56, + 0x6915c6e1, 0xf1407f39, 0x1b4dc5ab, 0xe90a3181, 0x3b1b4237, 0x21c6d08d, 0x34302c14, 0x268d6fc1, + 0xeabe3b40, 0x3c172d27, 0xf7c5d584, 0x81960cc0, 0x504020ea, 0x9d8b60ae, 0xa2ce002f, 0xba4f47f3, + 0x6f16479e, 0x1018e5d1, 0xc0ab5040, 0xa2bf90bd, 0xe6bff8f9, 0xb9cd26cb, 0x9bc34bd5, 0x144504b0, + 0xdbfe2675, 0xb00241d0, 0x88e77fee, 0xae2d9caa, 0xa0043e00, 0x26bbf4ed, 0xdcc0f158, 0xe3320463, + 0x852b928a, 0x0ec870e9, 0x8f00c1f2, 0xcea95e20, 0xd45277ff, 0x2cad9a9d, 0x02a4a8c1, 0x4a709348, + 0x35d960bd, 0x62977023, 0x17a7ed8c, 0x07f5f5b5, 0xfd0d4b8f, 0x8d88f33c, 0xf5afb7dd, 0x3a0ea830, + 0x6ebf28cb, 0x2d589527, 0x77d0810c, 0x11b131e7, 0x54850763, 0x0fa61805, 0x8039574f, 0xa65555d6, + 0x22b1804c, 0xa9f631b1, 0x49d11eca, 0xd41f0638, 0xe3105eb8, 0xba76cc33, 0x944263f8, 0xc7a8c754, + 0xf8259880, 0x063f1f00, 0x70d71f92, 0xa8a79da5, 0xc0d3815b, 0x966ac88e, 0x104b3f8b, 0x23b49348, + 0xc42cc90d, 0xc24791c3, 0x15e11067, 0x2dbb2669, 0x90799c32, 0x881a6a48, 0xf26af51c, 0x8174cbdc, + 0x0bf0f061, 0x2509dd80, 0x56293b24, 0x8c81078c, 0xbd6a1c97, 0xf4d98c12, 0xccac4283, 0xa68c09e9, + 0x5f606274, 0x84923735, 0x2c0b8b05, 0xdba69401, 0xcb6555ee, 0xd60daf25, 0x410bd800, 0x9aca380e, + 0xd8641710, 0x4d0b1601, 0xab2ac09e, 0xa17fc4f7, 0xe4b2bd22, 0x90c4eb3f, 0x19d9ce91, 0x121cc72f, + 0xfa8510fa, 0xd34d8461, 0x98077550, 0x11628e29, 0x2ed959aa, 0xf1e7cadb, 0xced7e127, 0x6925aaf0, + 0x2fa509c1, 0xa52ba3c8, 0xaf0b81dd, 0x9f4d58ac, 0x7a55dec3, 0x5e69738a, 0x48d3b5d4, 0x14e6965d, + 0x76f3c984, 0x41e33a97, 0x29a0da19, 0x29bc38dc, 0xd5fc71d4, 0x05f5ffae, 0xc62da371, 0x96fc4c26, + 0x8a6dc63b, 0xb48950bb, 0xf1d3c5a8, 0xc6d55752, 0x60683191, 0x8a7cb79b, 0x4fa4ae55, 0xd1d61a8b, + 0x9cb0b232, 0x51fa1a49, 0xdb73638b, 0xd904c2d5, 0x3a14aac7, 0x620569f4, 0x08483f6b, 0x0db48f35, + 0x20532f6c, 0x293ced32, 0xb38ebf22, 0x7c89c73d, 0x2d7c11e8, 0x5bb567cd, 0x61b32037, 0xad1cd53d, + 0x0e18b360, 0x3caf966e, 0xcb59ea4e, 0xe3a28284, 0xa8cb8e8b, 0x05747eb6, 0x75587f24, 0xf2128c81, + 0x1b34e7b4, 0x8e549407, 0x6ca142f4, 0x1deac038, 0x2a2f303b, 0x0ba4453d, 0xf8d1958f, 0x75e85141, + 0xd62ed92e, 0x7abaf0d5, 0x87edc08b, 0xc50b0e48, 0x3c49caf8, 0x58689ef9, 0x91878dd8, 0x72a93c45, + 0x347e087a, 0x4dbf035e, 0xff4102d6, 0x2f9a9185, 0x000fc519, 0x7b071fa0, 0x3e55e34d, 0x54606a4f, + 0x16482630, 0xce0f07ad, 0x01c43d49, 0x51fe3801, 0x857149e5, 0xd9d64938, 0xcb7da3f5, 0x9952fbe3, + 0x3729cd74, 0xdc27c37c, 0x9b9f8775, 0xb27514a3, 0xe9ce038c, 0xe295a7a4, 0x189660b3, 0x99907475, + 0x2082078d, 0x24a94c81, 0x3750e80d, 0xe4b49973, 0xe1372b52, 0x5a79bac1, 0x6f339514, 0x8eaa289e, + 0x70e91bd9, 0x5a7e9c2d, 0x1211b151, 0x0a32044b, 0xa13fe853, 0x79b9ff01, 0xc3decea2, 0x3c012018, + 0x6afd6f75, 0x7517d400, 0xd7598668, 0x61bdb52a, 0x413287d8, 0xe8cd2377, 0x3309bdaf, 0x7330afbb, + 0x9df48ce6, 0xa5cb783a, 0x4b9f8283, 0x311846c9, 0x8b6b29b7, 0x37928233, 0x88ac45da, 0x087f99c0, + 0xdf41affe, 0x2afe6c63, 0xf2d61a1c, 0x14b1b097, 0xec334f3f, 0x1da30414, 0xae8fe1c9, 0x67acd73f, + 0xfb118ed8, 0xe4213729, 0x1298038d, 0x2c47acff, 0x7d641646, 0x51efdbfe, 0xf2aada5b, 0xe59c9373, + 0x236f605c, 0x93994fed, 0x66c47cfd, 0x48061a01, 0x7ed7137d, 0xa01688a7, 0xa5f4a562, 0xef83222b, + 0xc36ec867, 0xb932238e, 0x92f5bbea, 0x26d91ab2, 0x708ac4dc, 0xdc51bf13, 0xc27f8304, 0x7190df40, + 0x609d50b3, 0x5718c7ea, 0x7bbf1081, 0xb151c2aa, 0x3b0174fd, 0xc1fd2578, 0xeb440352, 0xa297f85a, + 0xcbe54778, 0x0343bdf0, 0x3de413b1, 0x37443a5b, 0x603384a7, 0x76456c88, 0x28c4948b, 0xb34a83c5, + 0x63fe9591, 0xce3ef40f, 0x475a2a6a, 0xf4175d2c, 0x3d831006, 0x556eb1bc, 0xb67370b3, 0x0c689b90, 0x91eac424, 0x7a84a8c4, 0x67843a5f, 0x0e83fd51, 0xfad357ab, 0x16af5c67, 0x8c105104, 0xa80042a0, 0x4bff0748, 0x77c8f2b5, 0x71ee0e15, 0x8824cd0c, 0xe5439198, 0xe90a6e74, 0x833e5cdc, 0x93145fbb, 0x8d3dd6d5, 0x33a154ff, 0x6658b418, 0x0cfb9c00, 0x964f4f5c, 0x0a5dc760, 0xd08496ce, 0x293ac2c8, @@ -116,19 +122,205 @@ inline const char* ota_target_env_name(uint32_t target_id, char* buffer, size_t 0x396fecc7, 0xf94e3407, 0xe1930103, 0xa4740508, 0x9f19bcd1, 0xfcebff61, 0xac5c1405, 0xccb734e2, 0x634ff632, 0xf967a300, 0xa0cc1c36, }; +#if defined(OTA_RAK15002_SD_STORE) && defined(COMPANION_RADIO_FULL) + enum { DECODE_CHUNK_BYTES = 4096 }; static const uint16_t OFFSETS[] = { - 0, 227, 448, 665, 896, 1112, 1319, 1536, 1768, 1937, 2118, 2349, - 2591, 2847, 3082, 3330, 3535, 3739, 3912, 4128, 4330, + 0, 1066, 2165, 2715, }; static const uint16_t RAW_LENGTHS[] = { - 510, 512, 505, 509, 505, 512, 494, 506, 512, 499, 511, 507, - 507, 509, 481, 501, 498, 511, 488, 388, + 4087, 4061, 2042, }; static const uint16_t FIRSTS[] = { - 0, 27, 64, 95, 121, 158, 192, 226, 262, 295, 330, 366, - 398, 431, 463, 486, 522, 554, 596, 628, 659, + 0, 274, 536, 683, }; - // 4330 encoded bytes; 20 independent chunks, at most 512 decoded bytes. + // 2715 encoded bytes; 3 independent chunks, at most 4096 decoded bytes. + static const uint8_t NAMES[] = { + 0xad, 0x57, 0x57, 0x9b, 0xa3, 0x38, 0x10, 0xd4, 0xe5, 0xbb, 0xc9, 0x39, 0x3a, 0x6f, 0xce, 0x36, + 0xde, 0xf8, 0x76, 0x69, 0x73, 0xce, 0x8f, 0xfd, 0xc9, 0x20, 0xaf, 0xf9, 0x0c, 0x88, 0x93, 0x34, + 0xe9, 0xdf, 0xdf, 0x08, 0x31, 0xb8, 0xd1, 0xca, 0x6c, 0xde, 0x09, 0xc2, 0x1a, 0xba, 0xaa, 0xab, + 0xab, 0x1b, 0x44, 0xee, 0x0e, 0x8e, 0x14, 0x83, 0xbb, 0xfc, 0x15, 0xbd, 0xf2, 0xba, 0x0f, 0x3e, + 0x8f, 0x53, 0x9a, 0x84, 0x3c, 0x01, 0x41, 0x83, 0x90, 0xc3, 0x20, 0x62, 0xa4, 0xb1, 0x27, 0x07, + 0x64, 0x61, 0x1c, 0x4a, 0x09, 0x31, 0x0f, 0x58, 0x4c, 0x16, 0x5e, 0xb1, 0x94, 0x51, 0xc5, 0x04, + 0x59, 0x10, 0xf9, 0x0a, 0xf8, 0x40, 0x32, 0xb1, 0xcf, 0x04, 0xc4, 0xff, 0x29, 0x45, 0x16, 0x39, + 0x8f, 0xc1, 0x7c, 0x40, 0xb6, 0xad, 0xbd, 0x05, 0xc5, 0x44, 0x1c, 0x26, 0x34, 0x02, 0x7f, 0x44, + 0x15, 0x21, 0xf7, 0xff, 0x7a, 0x73, 0xe3, 0xa6, 0x07, 0xfd, 0xee, 0x6b, 0x78, 0xca, 0xe4, 0x08, + 0x1e, 0x87, 0xca, 0x49, 0xe3, 0xb4, 0xa6, 0xb1, 0x86, 0x68, 0xac, 0x15, 0xe0, 0x11, 0x17, 0x14, + 0xb8, 0xa2, 0x90, 0x70, 0x60, 0x87, 0x8a, 0x09, 0x1d, 0x5d, 0xb2, 0x44, 0x72, 0x21, 0xc9, 0x3a, + 0xe2, 0xf2, 0x5b, 0x86, 0x70, 0xf7, 0xdd, 0xdf, 0xf0, 0x42, 0x70, 0x70, 0xc5, 0x22, 0xb5, 0xcf, + 0x8f, 0x36, 0xf7, 0x46, 0x50, 0x7f, 0xcc, 0x84, 0x0e, 0xe6, 0xa4, 0x7c, 0x5e, 0x53, 0xde, 0x42, + 0x30, 0x5b, 0x9f, 0x47, 0x79, 0x1b, 0x83, 0xbc, 0xa7, 0xca, 0x1f, 0xf5, 0xdc, 0xa5, 0xc1, 0x72, + 0xfc, 0xc0, 0x12, 0x26, 0x42, 0x1f, 0xee, 0x7a, 0x1e, 0xce, 0x6c, 0x4e, 0x1e, 0xf6, 0xbc, 0x63, + 0x7d, 0x8b, 0x04, 0x97, 0x6f, 0x4f, 0xd6, 0x7f, 0xbc, 0x7e, 0xf1, 0xec, 0xf9, 0xfb, 0x2c, 0x74, + 0x1e, 0x55, 0x33, 0x5e, 0x34, 0xfb, 0x82, 0x2c, 0x65, 0x3c, 0xf6, 0x05, 0x59, 0x2c, 0xd7, 0xec, + 0x87, 0xbb, 0x2f, 0x5e, 0xfd, 0x03, 0x4f, 0xc2, 0x64, 0x3c, 0x8c, 0xf8, 0x01, 0xdc, 0xe9, 0x76, + 0x27, 0x21, 0xc9, 0x03, 0x16, 0x29, 0xe6, 0x83, 0xaf, 0x6e, 0x7a, 0x4e, 0xce, 0x4d, 0x48, 0x25, + 0xa9, 0x69, 0x9c, 0x39, 0x4c, 0xb3, 0x08, 0xb0, 0x06, 0x03, 0x11, 0x06, 0x1f, 0x18, 0x30, 0x99, + 0x26, 0xfc, 0x80, 0xcc, 0x19, 0x55, 0xc8, 0x6f, 0x77, 0x3d, 0xb7, 0x0a, 0x5f, 0x12, 0x6c, 0xbd, + 0xec, 0xc5, 0x79, 0x24, 0xf4, 0xa6, 0xe5, 0xd3, 0x3f, 0xee, 0x74, 0x11, 0x9a, 0xc6, 0xd9, 0xd0, + 0x38, 0xdb, 0x7a, 0xf9, 0xfd, 0xd1, 0x7e, 0x8b, 0xb5, 0x33, 0x25, 0x8f, 0xa8, 0x40, 0x5a, 0x8e, + 0x8c, 0x96, 0xc2, 0xef, 0xbb, 0xb5, 0xac, 0x0d, 0xf7, 0xa2, 0xc8, 0xe4, 0x5f, 0x3b, 0x08, 0x87, + 0xe1, 0x77, 0xe7, 0x55, 0xa8, 0x3f, 0x57, 0x76, 0xc0, 0xdc, 0x41, 0xa8, 0x46, 0x7c, 0x4f, 0x41, + 0x10, 0xca, 0x34, 0xa2, 0x47, 0x4e, 0x76, 0x97, 0x33, 0x76, 0x97, 0x35, 0xbb, 0xcb, 0x19, 0xbb, + 0x5a, 0xc1, 0xe8, 0xb4, 0xc5, 0xe8, 0x4c, 0x19, 0xb5, 0x8e, 0x18, 0x9d, 0xb3, 0x18, 0xd5, 0x0c, + 0xa3, 0xc2, 0x68, 0xaa, 0x7b, 0xe7, 0xa6, 0x0b, 0x1e, 0x86, 0x2c, 0xe6, 0xc3, 0x21, 0x39, 0x9d, + 0x64, 0x02, 0xa1, 0x4b, 0xa7, 0x48, 0x27, 0x84, 0x84, 0xf4, 0xfa, 0xde, 0x27, 0x66, 0x40, 0xd5, + 0x2e, 0x56, 0xb3, 0x50, 0xef, 0xf7, 0x5e, 0xef, 0x7a, 0xb5, 0x75, 0xab, 0xf9, 0x7c, 0x05, 0xe4, + 0xe7, 0x55, 0x48, 0xc3, 0xdb, 0x55, 0x29, 0x40, 0xcf, 0x54, 0x82, 0xe2, 0x1a, 0xfd, 0xf6, 0xa6, + 0x77, 0xa7, 0xeb, 0x2c, 0x82, 0xd1, 0x9e, 0xb4, 0xb2, 0xb5, 0x3b, 0x51, 0xb2, 0x5e, 0xf2, 0x02, + 0x54, 0xd8, 0x13, 0xc8, 0x96, 0xd5, 0x36, 0xaa, 0x07, 0x9f, 0x37, 0x53, 0x7f, 0xe7, 0x07, 0x4c, + 0xc0, 0x3e, 0x1a, 0x84, 0x5b, 0xd5, 0x37, 0x2c, 0xc9, 0xc0, 0xa7, 0x22, 0xf8, 0xbc, 0xf0, 0x45, + 0xab, 0xaa, 0xfc, 0x91, 0xb0, 0xdf, 0x03, 0xc4, 0xac, 0x44, 0x1a, 0x3d, 0x43, 0xec, 0x9d, 0x25, + 0xaf, 0xca, 0xc9, 0x17, 0x13, 0xd2, 0xd1, 0x2d, 0x95, 0x5d, 0x1f, 0xaf, 0xb5, 0xb2, 0x68, 0x4b, + 0xb7, 0xd8, 0xc9, 0xf5, 0xa5, 0x84, 0xac, 0x20, 0xb1, 0x57, 0x8a, 0xa4, 0x77, 0xac, 0xc6, 0xeb, + 0x98, 0x52, 0xef, 0x96, 0x79, 0xac, 0xa2, 0xda, 0xd6, 0x2d, 0x8e, 0x2b, 0xb9, 0xc1, 0x56, 0x4a, + 0x13, 0xa1, 0x68, 0xc7, 0x7d, 0xf7, 0xa4, 0xaa, 0xeb, 0xa9, 0xbf, 0xa3, 0xed, 0xb6, 0xa3, 0x79, + 0x92, 0x19, 0xc4, 0x6e, 0xa6, 0x60, 0x37, 0x8b, 0x70, 0x67, 0xca, 0xf1, 0x7f, 0xef, 0x57, 0xc4, + 0xd5, 0xaa, 0xe0, 0xe8, 0x0d, 0x43, 0xd5, 0x0d, 0xb2, 0x62, 0x49, 0xb0, 0x63, 0x24, 0x58, 0x2d, + 0xa7, 0xd9, 0x02, 0x19, 0xc6, 0x25, 0x42, 0xeb, 0x96, 0x10, 0x33, 0x46, 0x08, 0x8b, 0x68, 0xe1, + 0x84, 0xfd, 0xeb, 0xe0, 0xe9, 0x1f, 0x1f, 0xf3, 0x46, 0x35, 0x9c, 0xe9, 0x57, 0x15, 0xbc, 0x95, + 0xca, 0x7c, 0x69, 0x26, 0x7d, 0x71, 0x81, 0x2a, 0x5f, 0xc3, 0x65, 0x6f, 0x6b, 0x62, 0x27, 0x17, + 0x73, 0xec, 0xf0, 0x38, 0xb0, 0x3c, 0x8e, 0x3c, 0x0e, 0x15, 0xa8, 0xa1, 0xaa, 0x82, 0x9a, 0xfb, + 0xc4, 0x7e, 0x0b, 0xe3, 0xb7, 0x10, 0x7e, 0x0b, 0xe3, 0xcf, 0xb8, 0x4b, 0x94, 0xef, 0x76, 0x12, + 0xd2, 0xc8, 0x33, 0x4a, 0x4c, 0xb5, 0xcc, 0x96, 0xfe, 0xb8, 0x48, 0xf4, 0x4c, 0x62, 0x6a, 0x98, + 0xef, 0x34, 0x4d, 0x46, 0x46, 0x2c, 0x9c, 0xd0, 0xa4, 0x8f, 0x9b, 0x56, 0x5d, 0xb6, 0xa6, 0xb6, + 0xd7, 0x66, 0x55, 0x92, 0x09, 0xb9, 0x96, 0x4a, 0x72, 0x09, 0x17, 0x07, 0xdb, 0x27, 0xa5, 0x42, + 0x85, 0x4a, 0xdf, 0x96, 0xb1, 0x08, 0xb4, 0x43, 0xc5, 0x6d, 0xa7, 0x27, 0x5b, 0xda, 0x93, 0xf5, + 0xcc, 0x93, 0xf5, 0x2c, 0xcb, 0xcc, 0x93, 0xf3, 0x28, 0xda, 0x7c, 0x41, 0xde, 0x36, 0xd5, 0x02, + 0x32, 0xdc, 0x96, 0xb5, 0x37, 0x9f, 0x1b, 0x6e, 0xbe, 0x6c, 0xb8, 0x05, 0x77, 0x52, 0xa4, 0x9d, + 0x31, 0x68, 0x6b, 0x06, 0xed, 0x8c, 0xc1, 0x32, 0x62, 0xb0, 0x9c, 0x72, 0xa1, 0x04, 0x45, 0x3a, + 0x5a, 0x60, 0xbb, 0x53, 0x55, 0x5c, 0x2e, 0xc8, 0x6f, 0xdb, 0xa3, 0x01, 0x91, 0xaf, 0xd9, 0x77, + 0xe5, 0xe4, 0x97, 0xcb, 0xe4, 0x67, 0xa7, 0x75, 0xe5, 0x6a, 0xc4, 0x3e, 0x50, 0xff, 0x08, 0x26, + 0xba, 0x2b, 0x26, 0x95, 0xdd, 0xa5, 0x5f, 0xd1, 0x9f, 0xb3, 0x95, 0x1e, 0x68, 0xa0, 0xfa, 0xeb, + 0x83, 0xcf, 0xc9, 0x12, 0x59, 0x72, 0xa1, 0xc8, 0x7f, 0xc3, 0xe2, 0x8c, 0x8f, 0x3d, 0x0b, 0x39, + 0xfa, 0x82, 0x7b, 0x4c, 0xbe, 0x0f, 0x05, 0x8b, 0x98, 0x94, 0xf0, 0x82, 0xa6, 0x4c, 0x38, 0xeb, + 0x77, 0x41, 0xdb, 0xe8, 0xac, 0x2e, 0xdf, 0x06, 0x2a, 0xdc, 0x46, 0x81, 0xde, 0xb0, 0xd0, 0x9b, + 0x96, 0xd9, 0x11, 0x99, 0xb6, 0xfd, 0x9c, 0x31, 0x27, 0x17, 0x37, 0xee, 0x25, 0x8d, 0x3b, 0xfd, + 0xe8, 0x42, 0x5a, 0x16, 0x6e, 0xbb, 0x1c, 0x1b, 0x1f, 0x5e, 0x4e, 0x59, 0xb8, 0xbf, 0xbf, 0x7e, + 0xd2, 0xaf, 0x3e, 0x14, 0x98, 0x79, 0x56, 0xf1, 0xee, 0x5a, 0x33, 0xd3, 0xfa, 0x2b, 0x5e, 0x61, + 0x49, 0x38, 0xe6, 0x63, 0x0a, 0x23, 0x9a, 0x04, 0x23, 0x16, 0x05, 0x90, 0x88, 0x21, 0x30, 0x4f, + 0x9f, 0x3c, 0x83, 0x41, 0x0c, 0x53, 0xde, 0x22, 0x49, 0x2f, 0xeb, 0x1f, 0xc1, 0x15, 0x55, 0x2c, + 0x70, 0xfe, 0xc5, 0x9d, 0xac, 0xc7, 0x27, 0x65, 0x41, 0x54, 0x56, 0x91, 0x80, 0xbf, 0x26, 0x34, + 0xe1, 0x19, 0xa8, 0xe7, 0x69, 0x40, 0x57, 0xa8, 0x33, 0x3a, 0xd4, 0x3a, 0xba, 0x69, 0x7d, 0xe2, + 0x35, 0x14, 0x76, 0x51, 0x53, 0xfe, 0xa6, 0x08, 0xff, 0x03, 0xbd, 0x96, 0x05, 0x93, 0xe3, 0x38, + 0x10, 0x85, 0xb5, 0x98, 0xcc, 0x62, 0x38, 0xcb, 0x70, 0xcc, 0x67, 0x58, 0x2c, 0x3c, 0x5a, 0xe6, + 0xc1, 0xe2, 0x2e, 0xc5, 0xe9, 0x49, 0x54, 0xb1, 0xa5, 0xac, 0xa4, 0x0c, 0xfd, 0xfa, 0x1b, 0x85, + 0x2c, 0xab, 0x14, 0x1f, 0x5f, 0x71, 0x62, 0xab, 0xdf, 0xd7, 0xfd, 0xba, 0xe5, 0x26, 0x6c, 0x24, + 0x46, 0x14, 0x38, 0xe5, 0x02, 0xb8, 0xdc, 0x85, 0x38, 0xee, 0xf7, 0x32, 0x48, 0x44, 0x36, 0xa6, + 0x9c, 0x09, 0x0e, 0x92, 0xf6, 0x99, 0x80, 0x5e, 0x8a, 0xe4, 0xf3, 0x89, 0xea, 0x91, 0xf6, 0x88, + 0x29, 0x05, 0x99, 0xe8, 0x63, 0x46, 0xda, 0x12, 0xc7, 0x48, 0x35, 0x4a, 0xd2, 0x11, 0x22, 0x03, + 0x85, 0x72, 0x0f, 0x25, 0x39, 0xab, 0x34, 0x4b, 0x46, 0xd3, 0x58, 0x51, 0xb4, 0x2a, 0xd6, 0x17, + 0x26, 0x56, 0xc7, 0x8a, 0xd5, 0x59, 0xc6, 0xea, 0x5a, 0xb1, 0x6a, 0x71, 0xf0, 0x0f, 0x23, 0xd4, + 0xe3, 0x7f, 0x18, 0x80, 0xbc, 0x42, 0x1c, 0x6b, 0x44, 0xfe, 0x7a, 0x33, 0x84, 0xe5, 0x1b, 0x2d, + 0x48, 0x85, 0xa4, 0x20, 0x34, 0x05, 0x2e, 0x00, 0x0f, 0x34, 0x4a, 0x4e, 0x53, 0x50, 0xc8, 0x95, + 0x90, 0x8a, 0x90, 0xd7, 0x2c, 0x3d, 0x7c, 0x26, 0x60, 0xf3, 0xfb, 0x27, 0xc9, 0x50, 0x7c, 0xff, + 0x9a, 0x69, 0xf4, 0x32, 0x7c, 0x6a, 0x18, 0x9a, 0x16, 0x43, 0x93, 0x0b, 0x0e, 0x6a, 0x88, 0x69, + 0xea, 0x7d, 0xff, 0xfb, 0xe9, 0xfb, 0x39, 0x85, 0xc5, 0x79, 0x11, 0x7e, 0xa5, 0xb2, 0xff, 0x27, + 0x55, 0xbc, 0x11, 0x2e, 0xf9, 0xce, 0xde, 0x32, 0x67, 0x2f, 0x59, 0x67, 0x2f, 0x2d, 0xcf, 0x5e, + 0xb6, 0xce, 0x56, 0xe3, 0x8d, 0x18, 0xd4, 0x41, 0x18, 0x3d, 0x88, 0xfc, 0xc5, 0x86, 0xb1, 0x2a, + 0xe3, 0xb8, 0x0e, 0x3d, 0xc9, 0xfa, 0x03, 0x04, 0x54, 0x63, 0x2e, 0xf6, 0xc9, 0x0d, 0xd1, 0x9b, + 0x45, 0x86, 0xec, 0xa3, 0xd6, 0x05, 0xcc, 0x5b, 0x50, 0x7c, 0xd6, 0xd4, 0x28, 0x33, 0x66, 0xaa, + 0x9f, 0x0c, 0xa9, 0x26, 0xb5, 0x87, 0x0f, 0xbc, 0x04, 0xff, 0x8b, 0x78, 0x15, 0x5e, 0x64, 0xe3, + 0x49, 0xaa, 0x10, 0xde, 0xa7, 0x13, 0x95, 0xb7, 0x4b, 0xf5, 0x17, 0xa4, 0x19, 0x84, 0x3b, 0x7e, + 0x32, 0x53, 0x9b, 0x3b, 0xbb, 0x93, 0x34, 0x25, 0x77, 0x0c, 0xe4, 0x9d, 0x7d, 0xb6, 0xcb, 0x48, + 0xcd, 0x42, 0xad, 0x2d, 0x03, 0x5d, 0x71, 0x50, 0xaf, 0x16, 0x71, 0xea, 0x16, 0xea, 0x75, 0x07, + 0xb5, 0xfa, 0x1b, 0x26, 0x23, 0x2f, 0xc0, 0x4d, 0xa3, 0x7a, 0xd1, 0xd2, 0xbb, 0x98, 0x83, 0x3f, + 0xd9, 0x7c, 0x0e, 0x4f, 0x52, 0xa6, 0xb1, 0xcc, 0xdd, 0xaf, 0x4d, 0x80, 0xab, 0x56, 0x80, 0xab, + 0xcb, 0x00, 0x77, 0x1d, 0x8a, 0x6b, 0x16, 0xe1, 0x67, 0x2e, 0xe1, 0x74, 0xa6, 0x7e, 0xf5, 0xfa, + 0x07, 0xfe, 0x82, 0x80, 0x9d, 0x32, 0x90, 0xea, 0x6b, 0x13, 0x61, 0x3b, 0x82, 0x10, 0x42, 0x78, + 0xe0, 0x67, 0x35, 0xd5, 0x9e, 0xce, 0xfe, 0x17, 0xa6, 0xce, 0xfe, 0x1b, 0xc0, 0xf5, 0x19, 0x0a, + 0x57, 0xc2, 0x5d, 0xf7, 0x61, 0xa7, 0xd8, 0x05, 0xe4, 0xcd, 0xfd, 0x0d, 0x4d, 0x93, 0x11, 0x6c, + 0x71, 0xa6, 0x8f, 0xf3, 0x79, 0xed, 0x05, 0xb9, 0x6b, 0x18, 0xea, 0x96, 0x7a, 0x7d, 0xa9, 0xde, + 0xb0, 0xc4, 0x4e, 0xa0, 0x1a, 0xc2, 0x58, 0x24, 0x23, 0xd4, 0x7f, 0xfa, 0xfa, 0x39, 0x4d, 0xfb, + 0x7b, 0xc8, 0xf5, 0x44, 0xa2, 0x2c, 0xb1, 0xed, 0x4f, 0xde, 0x80, 0x6e, 0x76, 0xad, 0x47, 0xff, + 0x62, 0xb0, 0xd3, 0x2c, 0x63, 0x7e, 0xbb, 0xab, 0xb9, 0xc5, 0x24, 0x43, 0x35, 0xe4, 0x22, 0x15, + 0x83, 0x43, 0xd8, 0x0f, 0xfd, 0xb9, 0xdc, 0x9e, 0xce, 0xcf, 0x6d, 0x03, 0x71, 0x7b, 0xea, 0xeb, + 0x65, 0x0b, 0xe5, 0x72, 0x8e, 0xe2, 0xcc, 0xcf, 0x95, 0x62, 0x07, 0xd6, 0x2c, 0xd2, 0x6b, 0x4e, + 0x77, 0x5e, 0x9e, 0x55, 0x97, 0x5c, 0x76, 0xcd, 0x66, 0x1c, 0xf7, 0x15, 0x66, 0x0c, 0x32, 0x8c, + 0xee, 0xa7, 0x2a, 0x08, 0x21, 0xbf, 0x59, 0xca, 0x7d, 0x3a, 0xf1, 0xea, 0x09, 0xa8, 0xd8, 0x9b, + 0x4f, 0xd7, 0x64, 0xd2, 0x9d, 0x66, 0x52, 0xc9, 0x6f, 0x5c, 0x87, 0xbe, 0x2b, 0x55, 0x14, 0x47, + 0xa4, 0x6a, 0x51, 0x57, 0xe6, 0x94, 0x15, 0x87, 0x32, 0x13, 0x52, 0x21, 0xf4, 0x90, 0x27, 0x43, + 0x90, 0x74, 0x74, 0xef, 0x41, 0x1c, 0xe6, 0xba, 0xe4, 0x82, 0x0e, 0x1e, 0x3f, 0xb0, 0x7e, 0x9f, + 0x1d, 0x4b, 0xd1, 0x9f, 0x24, 0x7a, 0xc6, 0xe4, 0xbe, 0x4d, 0xde, 0x9a, 0x65, 0xe1, 0x59, 0x04, + 0x5b, 0xa9, 0x96, 0x34, 0xcf, 0x96, 0x70, 0xd6, 0x33, 0xde, 0xa9, 0x44, 0x22, 0x72, 0x48, 0x04, + 0xe7, 0x98, 0x68, 0xbf, 0xbd, 0x5f, 0x18, 0xbf, 0x60, 0xda, 0x35, 0x30, 0x9b, 0x44, 0xb0, 0xbb, + 0x07, 0xf2, 0xf6, 0x01, 0x72, 0xbb, 0x90, 0x75, 0x61, 0x16, 0xdd, 0xd9, 0x03, 0x52, 0x39, 0x42, + 0x29, 0xca, 0xb5, 0x3f, 0x9b, 0x69, 0x7f, 0x36, 0xd5, 0xfe, 0x6c, 0xa6, 0xdd, 0xca, 0xe5, 0x6e, + 0x3a, 0x72, 0x6d, 0x5b, 0xae, 0xe5, 0xc8, 0x91, 0xf7, 0x52, 0xbc, 0x61, 0x89, 0x14, 0x5e, 0x17, + 0xaf, 0x1a, 0x17, 0xd7, 0xac, 0x4e, 0x5c, 0x5b, 0x16, 0xab, 0xbe, 0xf0, 0x72, 0xea, 0xa1, 0x89, + 0xcf, 0x68, 0x1a, 0x92, 0x46, 0x69, 0xc3, 0x9c, 0xb3, 0x9c, 0x5e, 0x9b, 0x3b, 0xbd, 0xe6, 0x38, + 0xbd, 0x1e, 0xbe, 0x45, 0x3f, 0x4d, 0xc7, 0xd0, 0x9c, 0xb5, 0x68, 0xce, 0x2e, 0x69, 0x2e, 0x95, + 0x37, 0x6a, 0xa5, 0xb0, 0xdf, 0xcd, 0x74, 0xcf, 0x16, 0x75, 0x4f, 0xfc, 0xfc, 0x0a, 0xe2, 0x70, + 0xc5, 0x74, 0x5e, 0x35, 0x15, 0x9f, 0x55, 0xe3, 0xaa, 0xa9, 0x77, 0x79, 0x4d, 0xf2, 0xaf, 0xdb, + 0xac, 0xbf, 0x1b, 0xc5, 0x41, 0xb4, 0x8b, 0xd0, 0x72, 0x86, 0x74, 0x45, 0x51, 0xce, 0xdc, 0xb3, + 0xa6, 0xb2, 0x3c, 0xd5, 0x86, 0xa4, 0xa3, 0x30, 0x8e, 0x83, 0x08, 0xf6, 0xa3, 0xfb, 0x1f, 0xc3, + 0x07, 0xcb, 0xb7, 0x49, 0x63, 0xc2, 0x8f, 0xd1, 0xb1, 0x9f, 0xff, 0x73, 0xda, 0x0c, 0x86, 0x3f, + 0x61, 0xd4, 0x43, 0x94, 0x1c, 0xb5, 0x95, 0xba, 0x3f, 0x69, 0x6f, 0x1b, 0x94, 0x23, 0x7e, 0x1e, + 0x41, 0x79, 0x0a, 0x7f, 0x9c, 0xe0, 0xfd, 0x20, 0x08, 0x41, 0xa5, 0x42, 0x43, 0x92, 0xa7, 0x73, + 0x35, 0x02, 0xd5, 0x07, 0x85, 0xd8, 0x47, 0x09, 0x82, 0xa7, 0x87, 0x9e, 0x94, 0x1b, 0x6e, 0x55, + 0x2c, 0x37, 0x60, 0x91, 0x74, 0x39, 0x5e, 0xbb, 0xec, 0xe9, 0xc2, 0xc1, 0xf2, 0x10, 0x8e, 0xbd, + 0xa7, 0x77, 0x98, 0x7a, 0x63, 0xbe, 0x93, 0x9b, 0x74, 0xe0, 0xb5, 0xc3, 0xec, 0x55, 0x2b, 0x37, + 0xaa, 0x52, 0x25, 0x7b, 0xa3, 0xaa, 0xcd, 0x9b, 0x8b, 0x6c, 0x20, 0x57, 0xf8, 0x2b, 0x1d, 0xc3, + 0x86, 0x48, 0xa9, 0xb4, 0xd6, 0xbc, 0x17, 0xbc, 0xcf, 0x12, 0xaa, 0x85, 0xfc, 0xfe, 0xc9, 0xc6, + 0xfb, 0xb7, 0x62, 0x7f, 0x0a, 0x33, 0xe7, 0x30, 0x08, 0x8d, 0xd7, 0x62, 0x9d, 0x7e, 0xff, 0x36, + 0x7c, 0xb0, 0x1e, 0x39, 0x8f, 0x60, 0xb6, 0x8c, 0xb8, 0x07, 0x3e, 0x9f, 0xfd, 0x4f, 0x94, 0x11, + 0x3a, 0x60, 0x54, 0x40, 0x1c, 0x6c, 0xc0, 0x5f, 0x5c, 0xd4, 0x3b, 0x85, 0x1e, 0x2b, 0xec, 0xed, + 0x57, 0x73, 0x23, 0xcf, 0xc7, 0xff, 0x51, 0x60, 0xb2, 0xa1, 0xa9, 0x36, 0x31, 0x9f, 0x99, 0xbe, + 0x1d, 0x0c, 0x18, 0x1f, 0xc0, 0xca, 0xdd, 0xfb, 0xfc, 0xf2, 0x49, 0xd3, 0x79, 0xb2, 0x16, 0xc3, + 0x71, 0x51, 0xe3, 0xc8, 0xbf, 0x4c, 0x99, 0x29, 0x9b, 0xad, 0x54, 0x77, 0x4d, 0xc5, 0x8a, 0x8b, + 0x95, 0xab, 0x4a, 0xea, 0x32, 0xca, 0x7f, 0xdc, 0x70, 0xee, 0x90, 0xb6, 0x95, 0xc6, 0x6d, 0xe7, + 0x59, 0x6b, 0xde, 0x02, 0x8d, 0xc5, 0xe1, 0xab, 0xce, 0x0b, 0xf6, 0xea, 0xe6, 0x06, 0xae, 0x2f, + 0xfa, 0x47, 0x87, 0x41, 0x10, 0xe0, 0xea, 0xef, 0x7e, 0x25, 0x87, 0xb7, 0xbf, 0xfe, 0xe5, 0x9d, + 0xea, 0xdb, 0x02, 0x7e, 0x07, 0xb5, 0x54, 0x55, 0xb7, 0xdb, 0x30, 0x0c, 0xd6, 0x18, 0x0a, 0x63, + 0x66, 0x66, 0x48, 0x7a, 0xf1, 0x71, 0xcc, 0xcc, 0x4f, 0x3a, 0x4e, 0xab, 0xae, 0x3e, 0x4d, 0xec, + 0xcc, 0x4e, 0xe1, 0xe7, 0x2f, 0x2e, 0x78, 0x8e, 0xe7, 0xf1, 0xee, 0xab, 0x2d, 0x7d, 0x20, 0x82, + 0xb7, 0x78, 0x9b, 0x58, 0x86, 0x6f, 0x5a, 0xf8, 0x66, 0x90, 0x2b, 0xca, 0x08, 0xdf, 0x7c, 0x8c, + 0xe2, 0xa5, 0x18, 0xdb, 0x32, 0xcb, 0x99, 0xe0, 0x52, 0xa0, 0x62, 0x1d, 0x2e, 0x31, 0x49, 0x09, + 0xae, 0x62, 0xae, 0xe1, 0xd2, 0x88, 0x77, 0x39, 0x1c, 0xea, 0x73, 0xad, 0x31, 0x93, 0x1d, 0xca, + 0xe0, 0x90, 0xa2, 0x9c, 0x58, 0x41, 0x0a, 0x4e, 0x61, 0xa2, 0x78, 0xe7, 0x33, 0x21, 0xe9, 0x5c, + 0xc8, 0x11, 0x9c, 0x96, 0x89, 0x26, 0x35, 0x24, 0x85, 0xd9, 0x97, 0xa2, 0x80, 0xc3, 0x52, 0x66, + 0x38, 0x7d, 0x80, 0xb3, 0x58, 0xfd, 0x5b, 0x97, 0x18, 0x1d, 0x3f, 0x21, 0x3f, 0x69, 0xc8, 0x1b, + 0x0e, 0x6d, 0xc3, 0xd2, 0xee, 0xf3, 0xb0, 0x9a, 0x0e, 0xcf, 0x41, 0xef, 0x6f, 0xfb, 0xf2, 0xd2, + 0x5a, 0xc2, 0xaf, 0x23, 0xa1, 0x0b, 0xa6, 0xf0, 0x4e, 0x0b, 0xf5, 0x78, 0xe2, 0xc5, 0x81, 0xdc, + 0x6d, 0x21, 0x0f, 0x7b, 0x85, 0xda, 0xb9, 0xf2, 0x7b, 0x71, 0xeb, 0x7a, 0x5c, 0xf4, 0x9f, 0x97, + 0x41, 0xf8, 0x2c, 0x0a, 0xda, 0x38, 0x36, 0xd0, 0x49, 0xd8, 0x46, 0x45, 0x76, 0x2d, 0x0e, 0x67, + 0x77, 0x07, 0x69, 0x0a, 0xc7, 0xca, 0x20, 0xce, 0xd2, 0x29, 0xd6, 0x31, 0xd3, 0xee, 0x2a, 0xe2, + 0x6b, 0x5b, 0x18, 0x4f, 0x5d, 0x43, 0x39, 0x14, 0x8d, 0x32, 0x22, 0xe3, 0x82, 0xa5, 0xd8, 0xee, + 0xb1, 0x02, 0x6a, 0xad, 0x3f, 0xd5, 0xbb, 0x0f, 0x53, 0xa9, 0x18, 0xca, 0x82, 0xa1, 0x90, 0x48, + 0xe3, 0xf2, 0xcd, 0xa0, 0x69, 0x12, 0x5a, 0x2a, 0x5d, 0xb5, 0xb3, 0xf8, 0xe3, 0x9e, 0xae, 0xa5, + 0xa7, 0xa5, 0x7f, 0xea, 0xc1, 0x72, 0x38, 0x9b, 0x8a, 0x1e, 0x29, 0x41, 0xc5, 0x5c, 0xb9, 0xab, + 0xf8, 0x5f, 0xc7, 0xb3, 0xb6, 0x1a, 0xe2, 0xc4, 0x09, 0x09, 0x4e, 0x59, 0x30, 0x4c, 0x83, 0x2e, + 0x2e, 0x02, 0x7c, 0x60, 0x43, 0xd2, 0x3d, 0xa6, 0xc8, 0xdc, 0x8e, 0xa7, 0xaf, 0xa3, 0x28, 0x0e, + 0x0f, 0xe4, 0x59, 0x23, 0x7f, 0xaf, 0x83, 0xb8, 0x37, 0x67, 0xaa, 0xe0, 0x85, 0x89, 0xa2, 0x71, + 0x19, 0xde, 0x21, 0x85, 0x73, 0x12, 0x4f, 0xed, 0x45, 0xcb, 0xe8, 0xff, 0xec, 0xb5, 0xee, 0x8f, + 0x7a, 0x3f, 0xfb, 0x1c, 0xf7, 0x27, 0xfc, 0xe5, 0x7c, 0xf8, 0x29, 0xbe, 0x19, 0x2f, 0x62, 0xc2, + 0x4c, 0x83, 0x83, 0x85, 0x68, 0x7e, 0xf3, 0xbb, 0xae, 0x7c, 0x7b, 0xab, 0x58, 0xbb, 0x4f, 0x0a, + 0x9f, 0x86, 0xf7, 0xe5, 0x84, 0x31, 0xb7, 0xc3, 0x6a, 0xd9, 0xe9, 0x70, 0xc3, 0xa8, 0x8c, 0x19, + 0x65, 0x51, 0x14, 0xdd, 0x44, 0x1b, 0x00, 0x1f, 0xb8, 0x9c, 0x41, 0x3e, 0x8d, 0xd6, 0x70, 0x1f, + 0xea, 0xf7, 0xb8, 0xe8, 0x07, 0xf1, 0x77, 0x39, 0xd0, 0xf0, 0x91, 0x33, 0x69, 0x2e, 0x55, 0x28, + 0xf0, 0x88, 0xd9, 0x9d, 0x83, 0x46, 0xcd, 0xc1, 0xc9, 0xec, 0x6d, 0x75, 0x12, 0xb7, 0x5a, 0x4d, + 0xdb, 0x1c, 0xd2, 0xcd, 0xc1, 0x6d, 0xc0, 0x70, 0x26, 0xc2, 0x26, 0xa1, 0xba, 0x8b, 0x71, 0x98, + 0xdb, 0xd0, 0xd6, 0x9c, 0xb4, 0x9a, 0x25, 0xdc, 0x33, 0x5f, 0x4f, 0xa5, 0xe3, 0x56, 0x0c, 0x75, + 0x87, 0x7f, 0xd3, 0x9b, 0x5f, 0x39, 0x09, 0x7b, 0xd8, 0xe9, 0x6d, 0xfc, 0xae, 0xea, 0xd0, 0xb8, + 0x16, 0xf7, 0x78, 0x03, 0xb5, 0x75, 0x5a, 0x7c, 0xd8, 0xfa, 0xe1, 0xd1, 0x8b, 0xf0, 0x80, 0x18, + 0xee, 0xa3, 0x93, 0x3d, 0x3e, 0x3a, 0xbb, 0x40, 0x47, 0x8d, 0x92, 0xa3, 0x93, 0x9a, 0xd6, 0x1d, + 0x3d, 0xf5, 0xef, 0xd7, 0x02, 0xea, 0x56, 0xe3, 0x5e, 0x4f, 0xe3, 0xbe, 0xaa, 0x8e, 0x86, 0xa3, + 0xf1, 0x80, 0xa7, 0xb1, 0x3e, 0xd3, 0x58, 0xaf, 0xde, 0xae, 0xaf, + }; +#else + enum { DECODE_CHUNK_BYTES = 512 }; + static const uint16_t OFFSETS[] = { + 0, 227, 448, 665, 896, 1116, 1323, 1537, 1779, 1926, 2134, 2376, + 2609, 2878, 3114, 3328, 3569, 3806, 3999, 4200, 4442, 4662, + }; + static const uint16_t RAW_LENGTHS[] = { + 510, 512, 505, 509, 512, 500, 502, 511, 489, 512, 510, 504, + 505, 501, 486, 504, 499, 504, 504, 509, 460, + }; + static const uint16_t FIRSTS[] = { + 0, 27, 64, 95, 121, 159, 193, 227, 262, 295, 329, 364, + 396, 426, 461, 481, 510, 544, 578, 618, 647, 683, + }; + // 4662 encoded bytes; 21 independent chunks, at most 512 decoded bytes. static const uint8_t NAMES[] = { 0x8c, 0x51, 0x03, 0x6f, 0x40, 0x41, 0x0c, 0xee, 0x82, 0xd9, 0x76, 0x30, 0xdb, 0x88, 0x67, 0x9b, 0x71, 0x73, 0x68, 0xf2, 0xdc, 0x77, 0x98, 0x7e, 0xfd, 0x6c, 0x47, 0x77, 0xf5, 0x07, 0x58, 0x95, @@ -186,228 +378,257 @@ inline const char* ota_target_env_name(uint32_t target_id, char* buffer, size_t 0x0a, 0xa6, 0x65, 0xce, 0xf7, 0xe0, 0x37, 0xad, 0xd7, 0x4c, 0x44, 0x35, 0xa0, 0x57, 0xec, 0x62, 0xd6, 0x4b, 0x7a, 0xb3, 0x23, 0xa8, 0x6f, 0xb2, 0xf2, 0x36, 0xd4, 0x44, 0xd7, 0x49, 0xc8, 0x75, 0xe1, 0x2e, 0x85, 0x33, 0xb7, 0x10, 0xc7, 0xf5, 0xba, 0x9a, 0xd7, 0x9c, 0xed, 0x8c, 0xfe, 0x01, - 0x7d, 0x51, 0x35, 0x76, 0x03, 0x31, 0x10, 0xfd, 0x5d, 0xa8, 0x0a, 0x33, 0xf4, 0xa9, 0x0c, 0x87, - 0xc8, 0x2d, 0x66, 0x69, 0x14, 0xeb, 0x59, 0x2c, 0x79, 0x7d, 0x7c, 0x33, 0x7b, 0x59, 0xac, 0x3f, - 0x1f, 0x30, 0x62, 0x95, 0xb8, 0xa4, 0x14, 0xf2, 0x72, 0xcc, 0x81, 0xea, 0x3e, 0x95, 0x56, 0xbb, - 0xdc, 0x48, 0x6b, 0x28, 0xe4, 0x95, 0xb4, 0x34, 0x95, 0x42, 0x92, 0x60, 0x6d, 0x85, 0xc0, 0xaf, - 0xc1, 0xdd, 0x58, 0xc6, 0x48, 0xda, 0x56, 0xac, 0x71, 0x17, 0xd8, 0x71, 0x9e, 0x38, 0xe0, 0x8d, - 0x8a, 0x20, 0xab, 0x7f, 0x26, 0x8e, 0xce, 0xd8, 0x29, 0xde, 0x6d, 0x11, 0x39, 0xd4, 0x1c, 0x48, - 0xfb, 0x94, 0x70, 0x6f, 0xad, 0xa6, 0xf5, 0x02, 0x3e, 0xe9, 0x78, 0xef, 0x2e, 0xb2, 0x89, 0x36, - 0xe0, 0x2e, 0x71, 0xd0, 0xd2, 0xe4, 0x8a, 0xca, 0x51, 0x9e, 0x80, 0xbf, 0x35, 0xb3, 0x06, 0x46, - 0x85, 0xe2, 0x05, 0x8a, 0x8b, 0x78, 0x9b, 0xc4, 0x02, 0x6f, 0x4b, 0x82, 0xb8, 0x3e, 0xa0, 0x75, - 0xbd, 0xa3, 0x75, 0x73, 0x50, 0xf7, 0xfa, 0x18, 0xff, 0x72, 0xd0, 0x81, 0x2b, 0x26, 0x4a, 0x1d, - 0xa2, 0x7f, 0xad, 0xa9, 0x36, 0x17, 0xb9, 0x3b, 0xd1, 0xfe, 0x16, 0x62, 0x7f, 0xd0, 0xc7, 0xfd, - 0xb1, 0xcc, 0x1f, 0x8a, 0x52, 0x1f, 0x11, 0x7a, 0x3c, 0x31, 0xe2, 0x7a, 0x6d, 0xc4, 0x29, 0xd1, - 0x6d, 0x44, 0xf5, 0x90, 0xfa, 0xcb, 0xe6, 0x9c, 0xf7, 0x92, 0xed, 0x2a, 0x21, 0x83, 0xeb, 0x46, - 0x55, 0xdb, 0xf8, 0x7e, 0x5c, 0xdc, 0x0e, 0xe7, 0x85, 0x4f, 0x83, 0x6e, 0x44, 0x51, 0x10, 0x3d, - 0x71, 0x56, 0x61, 0x6d, 0x06, 0xb5, 0xdb, 0xbf, 0x99, 0xa7, 0xb9, 0xe9, 0xd3, 0xf5, 0xe2, 0xef, - 0xd7, 0x66, 0x34, 0x3c, 0xc2, 0x3f, 0x17, 0x9e, 0x63, 0x6a, 0x7d, 0xd1, 0x27, 0xc5, 0xaa, 0xd4, - 0xa1, 0x4c, 0x95, 0x24, 0x1b, 0x26, 0xa9, 0x22, 0xd1, 0x2c, 0x0a, 0x12, 0x5c, 0x2a, 0x21, 0x70, - 0xd6, 0x74, 0xd1, 0xb4, 0x6f, 0xa7, 0x22, 0x9d, 0x0c, 0xd7, 0xa5, 0xf1, 0x7e, 0x32, 0xd4, 0xb9, - 0xa3, 0x43, 0xe9, 0xfa, 0x14, 0x79, 0xea, 0xc9, 0x0b, 0xbf, 0xc2, 0x19, 0x15, 0x3c, 0x7d, 0xde, - 0x71, 0xbf, 0x9a, 0xd7, 0xbf, 0x9a, 0xd3, 0xbf, 0x9a, 0xd7, 0xaf, 0xae, 0x61, 0xc0, 0xf9, 0xf0, - 0x2a, 0x71, 0xa1, 0xdd, 0xb8, 0x3b, 0xe9, 0xe3, 0x97, 0xd7, 0x03, 0xf8, 0x7d, 0xff, 0x34, 0xa5, - 0x93, 0xb8, 0x9c, 0xa6, 0x91, 0xa8, 0x2e, 0x84, 0xb1, 0xac, 0x39, 0xf4, 0x6c, 0x71, 0x49, 0x2a, - 0x72, 0x6c, 0x5b, 0x6c, 0x69, 0xf0, 0x8c, 0x23, 0xa5, 0x4a, 0x1a, 0x2f, 0x16, 0x2f, 0x87, 0xdb, - 0x02, 0x4a, 0xbc, 0x6a, 0x87, 0xc7, 0x69, 0x46, 0x89, 0x6a, 0x9e, 0x3a, 0x47, 0xa5, 0x4a, 0xb8, - 0x44, 0x55, 0x87, 0xd6, 0xa7, 0x7e, 0x00, 0x1b, 0xba, 0x48, 0xd8, 0xa2, 0x6a, 0xff, 0xd6, 0xb1, - 0xe1, 0x8a, 0xb4, 0xc3, 0xf9, 0x80, 0x08, 0xe7, 0xc3, 0x94, 0x83, 0x3c, 0x68, 0xcc, 0xb1, 0x35, - 0xa6, 0xe6, 0xf7, 0x97, 0x2c, 0xf6, 0x00, 0x6c, 0x50, 0x45, 0x72, 0x05, 0x41, 0x08, 0x65, 0x1b, - 0x77, 0xf7, 0x33, 0x44, 0x6f, 0x43, 0xb5, 0x30, 0x7f, 0x18, 0xa3, 0x03, 0xc4, 0x4e, 0x1f, 0xd7, - 0xae, 0xac, 0x70, 0x9e, 0x40, 0x4b, 0x83, 0x53, 0xc2, 0xfb, 0x0b, 0xd4, 0x1b, 0x54, 0x91, 0x11, - 0x8d, 0xf4, 0x9e, 0x14, 0x76, 0x51, 0xe2, 0x47, 0x8a, 0xe3, 0xad, 0x3b, 0x2c, 0x19, 0x4d, 0x26, - 0x0a, 0x4b, 0x4e, 0x3a, 0xf2, 0x14, 0x06, 0x4c, 0x6d, 0x70, 0x58, 0x6e, 0x1c, 0x93, 0x8c, 0x25, - 0x4c, 0x2c, 0x13, 0x6a, 0xc8, 0x2c, 0x18, 0x07, 0x82, 0xb3, 0xe6, 0x6e, 0x18, 0xe0, 0xec, 0xce, - 0x22, 0x9c, 0x3d, 0x70, 0xc3, 0xb0, 0xd6, 0xb3, 0x19, 0x8e, 0x92, 0x69, 0x84, 0xb5, 0x22, 0xea, - 0x1a, 0xd8, 0x51, 0xa9, 0x50, 0x78, 0xfd, 0x58, 0x81, 0x1d, 0xfc, 0x10, 0xa9, 0x26, 0x6b, 0x5f, - 0x17, 0xb0, 0x57, 0x4d, 0xd6, 0x7f, 0x91, 0x3f, 0xac, 0xaf, 0x3e, 0xc9, 0xaf, 0xfd, 0x25, 0xbf, - 0xf0, 0xf5, 0x6b, 0x1d, 0xa3, 0x72, 0x9e, 0x11, 0x92, 0x95, 0x49, 0x1e, 0x60, 0x63, 0xa0, 0x59, - 0x48, 0x4f, 0x58, 0x82, 0x3a, 0xfb, 0x9b, 0x32, 0x27, 0x73, 0xd8, 0xf8, 0xfb, 0x75, 0xe1, 0x17, - 0xe2, 0x56, 0x85, 0x38, 0xff, 0x89, 0x38, 0x5f, 0x21, 0xfe, 0x6f, 0x17, 0x36, 0x34, 0xca, 0x04, - 0xc7, 0x6f, 0xa6, 0x7d, 0xe5, 0xaf, 0xd6, 0xbd, 0x0c, 0xc6, 0x04, 0x05, 0x20, 0x94, 0xcd, 0x07, - 0xf7, 0xbf, 0x18, 0x9a, 0x9b, 0xf9, 0x91, 0x5c, 0xc3, 0x07, 0xb5, 0x9d, 0x0f, 0xd5, 0x76, 0x00, - 0x74, 0xce, 0x83, 0x72, 0x6c, 0x41, 0x10, 0x06, 0xe0, 0x2e, 0x5e, 0xc4, 0xb6, 0x6d, 0xeb, 0x09, - 0x62, 0xab, 0xdc, 0x35, 0xe8, 0xcd, 0xce, 0xd1, 0x9c, 0xf4, 0x4c, 0xb2, 0xaf, 0x9f, 0xac, 0x5d, - 0x6e, 0x7e, 0x3f, 0x5c, 0x50, 0xe4, 0x49, 0xe1, 0xbb, 0x61, 0x8a, 0xc8, 0x39, 0x7c, 0x10, 0x29, - 0x31, 0x2a, 0x1b, 0xa7, 0x22, 0x31, 0x36, 0x41, 0x16, 0xda, 0x58, 0x94, 0x11, 0xc1, 0x1a, 0xa6, - 0x0e, 0x96, 0xbf, 0x9c, 0x84, 0xe1, 0xd0, 0xfc, 0x6e, 0xc6, 0x56, 0x53, 0x0c, 0xc3, 0x4c, 0x29, - 0x09, 0x4f, 0x0c, 0xd3, 0x28, 0xd9, 0xe8, 0x0f, 0x42, 0x72, 0x69, 0x62, 0x73, 0x30, 0x63, 0xa5, - 0x23, 0xfe, 0x26, 0xc6, 0xf8, 0xd3, 0x7b, 0x18, 0xb1, 0x36, 0xc6, 0x62, 0x03, 0xe6, 0xb0, 0x7e, - 0xd6, 0xfb, 0xc2, 0x42, 0x85, 0x6d, 0xdc, 0x8d, 0xbc, 0xbb, 0x9a, 0x77, 0x47, 0x6b, 0xdc, 0xd1, - 0x8a, 0x3b, 0xdb, 0xe0, 0xce, 0xd5, 0xff, 0x1e, 0xab, 0x71, 0x17, 0x1a, 0xdc, 0xbf, 0xcf, 0x37, - 0x07, 0x2d, 0xcd, 0x99, 0xbc, 0x39, 0x99, 0x37, 0x27, 0x73, 0x26, 0x63, 0xa0, 0xb3, 0xa2, 0x0d, - 0x36, 0x68, 0x93, 0xec, 0xf6, 0x0f, 0xf6, 0x61, 0xa8, 0xfe, 0x71, 0x57, 0x0d, 0x3a, 0xd2, 0x80, - 0x76, 0x3a, 0x4a, 0x9c, 0x65, 0x00, 0x13, 0xda, 0x50, 0x60, 0x56, 0x24, 0x3a, 0x4b, 0x91, 0xc6, - 0x84, 0x33, 0x48, 0xfb, 0xfb, 0x78, 0xb0, 0xab, 0x65, 0x8c, 0xbb, 0x67, 0xc7, 0x2d, 0x93, 0xed, - 0xe5, 0x43, 0xcd, 0xb1, 0xf5, 0xc2, 0x93, 0x6e, 0xb9, 0x71, 0x96, 0xdf, 0xf8, 0x99, 0x2c, 0x22, - 0x5a, 0x90, 0x9c, 0x22, 0x8c, 0x14, 0x80, 0x00, 0xa5, 0xd1, 0xb1, 0x4a, 0xc0, 0x40, 0x0c, 0x80, - 0xe1, 0x4c, 0xae, 0x2a, 0xd8, 0xfa, 0x04, 0xba, 0x15, 0x6a, 0xfb, 0x0c, 0xe2, 0x60, 0xc7, 0xee, - 0x21, 0x6d, 0x23, 0x1e, 0x77, 0x97, 0x94, 0xe4, 0x14, 0x7d, 0x7b, 0xeb, 0x52, 0x14, 0x04, 0x87, - 0x5b, 0x43, 0xf8, 0xf8, 0x43, 0x20, 0x44, 0x8d, 0x84, 0x42, 0xa2, 0x28, 0xf6, 0x82, 0xc3, 0xb0, - 0x2d, 0x19, 0x57, 0xcd, 0x3b, 0x49, 0x50, 0x41, 0xa3, 0x2d, 0x28, 0x2e, 0x89, 0xe1, 0xee, 0xcd, - 0x17, 0x68, 0x62, 0x70, 0xc7, 0xac, 0x1b, 0x67, 0x68, 0x8c, 0x77, 0xa6, 0xc2, 0x06, 0xad, 0x6a, - 0x46, 0x67, 0x7b, 0x67, 0x83, 0xcb, 0xb1, 0xaf, 0x14, 0xae, 0xc6, 0x4a, 0xe0, 0xc2, 0x4b, 0x58, - 0xe3, 0x3f, 0xe7, 0xdc, 0x7f, 0x5b, 0xed, 0x0f, 0xab, 0x3d, 0xad, 0xdb, 0x5f, 0x31, 0x7d, 0xa5, - 0x70, 0x3d, 0x56, 0x02, 0xf0, 0xcc, 0xbc, 0x17, 0x66, 0x99, 0xe6, 0x07, 0x3c, 0x37, 0x6e, 0x30, - 0xa9, 0x11, 0x6a, 0x21, 0x3c, 0x5e, 0xc7, 0x1f, 0xc7, 0x4c, 0x28, 0xa1, 0xb3, 0xb8, 0x9a, 0x03, - 0x4c, 0x21, 0x7d, 0x3e, 0x29, 0xce, 0xdd, 0xe3, 0xfa, 0xaa, 0xdd, 0x14, 0x0a, 0xff, 0xd9, 0xf0, - 0x05, 0x7c, 0x50, 0xb5, 0x5a, 0x06, 0x41, 0x0c, 0x4c, 0x87, 0x3b, 0x15, 0xee, 0x15, 0xae, 0x3d, - 0x6e, 0x1d, 0xf4, 0x39, 0xcb, 0xc7, 0xad, 0x66, 0x0d, 0x7b, 0x7a, 0x5c, 0x7f, 0xab, 0xc7, 0x07, - 0xae, 0x85, 0x7e, 0x3e, 0x63, 0xbc, 0x5d, 0x3b, 0x29, 0x6b, 0x5e, 0xbb, 0x16, 0x89, 0xb0, 0x64, - 0xe3, 0x72, 0x2b, 0xd8, 0x62, 0xc8, 0x2b, 0xc1, 0x78, 0x1f, 0x0b, 0x18, 0x53, 0x22, 0x46, 0x34, - 0x5c, 0x91, 0x81, 0x31, 0xfb, 0x06, 0xc5, 0x9a, 0xb4, 0x6e, 0xa2, 0x16, 0x9a, 0x60, 0xed, 0x83, - 0x1f, 0xc8, 0x51, 0x9e, 0x28, 0xc0, 0x38, 0xb3, 0xc1, 0x48, 0xe1, 0x81, 0x02, 0x0c, 0xe0, 0x51, - 0x1e, 0xaa, 0x96, 0xaa, 0xc5, 0xc6, 0x94, 0xd6, 0x0e, 0x83, 0xad, 0xb4, 0x33, 0xef, 0xda, 0xc1, - 0x3f, 0xda, 0xc1, 0x1f, 0xed, 0xd0, 0x1f, 0x6d, 0xf7, 0xce, 0xcd, 0x0e, 0xc6, 0xa7, 0xad, 0xed, - 0xfd, 0xed, 0x96, 0x15, 0x56, 0xd0, 0xc5, 0x4e, 0x3d, 0x26, 0xb1, 0x08, 0xa2, 0xba, 0x23, 0xa4, - 0xe8, 0x2c, 0x3f, 0xc2, 0x14, 0x17, 0x9f, 0xce, 0x68, 0x7c, 0x4a, 0xff, 0x6a, 0xce, 0xe0, 0x7f, - 0x6c, 0xec, 0x4d, 0x6f, 0x84, 0xcd, 0xdf, 0x1e, 0xab, 0xf3, 0x04, 0xc3, 0x07, 0xaf, 0x33, 0xc3, - 0xe6, 0x02, 0xfa, 0x58, 0x0e, 0x00, 0x6d, 0x90, 0xb5, 0x72, 0x04, 0x31, 0x10, 0x44, 0x27, 0x33, - 0x33, 0x33, 0x53, 0x66, 0xce, 0xcd, 0xcc, 0x14, 0xf6, 0xd2, 0x9c, 0x4b, 0x75, 0xd2, 0xce, 0x5a, - 0xda, 0x35, 0xfc, 0xbd, 0xeb, 0x18, 0x33, 0xe1, 0xeb, 0x37, 0x4d, 0xb7, 0x4a, 0xff, 0x5d, 0x08, - 0x5e, 0x71, 0x65, 0x92, 0x4c, 0x3b, 0xc6, 0xa3, 0xce, 0x1c, 0x2c, 0x27, 0xec, 0xa7, 0x6c, 0xa9, - 0xf3, 0x98, 0x7d, 0x83, 0x9d, 0x0f, 0x84, 0x62, 0x12, 0x3f, 0x56, 0x12, 0xc3, 0xfa, 0x91, 0x12, - 0x04, 0x9a, 0x69, 0x05, 0x89, 0xa3, 0xc5, 0x5c, 0xa6, 0x35, 0x2d, 0x66, 0x2e, 0xa0, 0xc5, 0x1f, - 0x95, 0x53, 0x34, 0x98, 0x57, 0xce, 0xc1, 0x48, 0xc4, 0x86, 0x06, 0xab, 0xa0, 0x49, 0x04, 0x56, - 0x45, 0x9f, 0x0c, 0x76, 0x49, 0x2c, 0x3f, 0x34, 0x25, 0x81, 0x63, 0xfb, 0xcd, 0x16, 0xe6, 0x2b, - 0x4d, 0x69, 0x48, 0xc4, 0xa0, 0x74, 0x40, 0x33, 0x68, 0xbc, 0xeb, 0x3c, 0xe5, 0x30, 0xdf, 0x56, - 0x60, 0xae, 0x90, 0xda, 0x57, 0x97, 0xd7, 0x57, 0x13, 0x3f, 0x7b, 0xbd, 0xc4, 0x99, 0x56, 0x29, - 0xc3, 0xfd, 0xee, 0xec, 0x1e, 0xee, 0xb6, 0x05, 0x6c, 0x15, 0x00, 0x53, 0x75, 0x80, 0xa9, 0x2a, - 0x60, 0xa9, 0xc9, 0x62, 0xba, 0xce, 0x70, 0xb5, 0xd9, 0x50, 0x8b, 0xf5, 0x71, 0x72, 0xd8, 0x2e, - 0x03, 0xed, 0x0b, 0x41, 0x6d, 0xe4, 0xc2, 0xa6, 0xf3, 0xb6, 0x40, 0x78, 0xdf, 0xc5, 0x0e, 0x76, - 0x70, 0xd8, 0xde, 0xb5, 0xd0, 0xf6, 0x7a, 0x41, 0x78, 0xbd, 0xd8, 0xf3, 0x78, 0x1d, 0x76, 0xbc, - 0xaa, 0x3d, 0xdf, 0xa8, 0x06, 0x9a, 0xa8, 0xf3, 0x5e, 0x6a, 0xbe, 0xfc, 0x07, 0xad, 0xcf, 0xb5, - 0x5a, 0x2c, 0x41, 0x10, 0x05, 0xe0, 0xba, 0x17, 0x8d, 0x70, 0x77, 0x8f, 0x71, 0x88, 0x71, 0x4d, - 0xb0, 0xf4, 0x7c, 0x23, 0xb5, 0x3b, 0x3d, 0xd2, 0x35, 0x2d, 0x6b, 0x4f, 0x8f, 0x3b, 0x84, 0xa4, - 0xa5, 0xff, 0xa1, 0x0b, 0x95, 0xb7, 0x8e, 0x05, 0x37, 0x17, 0x62, 0x03, 0xdc, 0xad, 0x63, 0x0d, - 0x6b, 0xd8, 0x86, 0x67, 0x5b, 0x28, 0x1d, 0xe4, 0x88, 0x92, 0xc0, 0x13, 0x5d, 0x6e, 0x5d, 0xfb, - 0x20, 0xca, 0x70, 0xab, 0x95, 0xc7, 0xfe, 0xf6, 0x05, 0x22, 0x29, 0xca, 0x40, 0x2b, 0xd1, 0xb0, - 0x41, 0xac, 0x04, 0x61, 0xce, 0x34, 0x5f, 0x73, 0x21, 0xf5, 0x67, 0xca, 0x39, 0x14, 0x12, 0x73, - 0x41, 0xfd, 0x96, 0x4b, 0x0e, 0x3c, 0x5b, 0x1a, 0x10, 0x29, 0xe0, 0xd8, 0xd6, 0xd9, 0xd2, 0x3f, - 0x76, 0x09, 0x4a, 0x89, 0x32, 0xf6, 0x78, 0x1f, 0x18, 0x42, 0xfe, 0xf4, 0x5f, 0x7c, 0x00, 0x2d, - 0xe0, 0xa6, 0x67, 0xfb, 0xf4, 0xdd, 0xb1, 0x76, 0x62, 0x1d, 0xb5, 0x07, 0x71, 0x9d, 0xb5, 0xaf, - 0x59, 0xb6, 0x70, 0xcd, 0xb5, 0xf5, 0xed, 0xf5, 0x5f, 0x05, 0x2b, 0x4f, 0x82, 0x91, 0x4f, 0x82, - 0x91, 0xf7, 0x07, 0xa3, 0x9f, 0x04, 0x23, 0x5f, 0xd3, 0x0d, 0xed, 0xfe, 0xe1, 0xb1, 0x76, 0x55, - 0xa8, 0xdf, 0xce, 0x81, 0xba, 0xdf, 0xd6, 0x41, 0x54, 0xb0, 0x4b, 0xb4, 0xe4, 0x52, 0x6d, 0xa1, - 0xb1, 0xf6, 0x7b, 0x96, 0xd9, 0x4a, 0x2d, 0xcf, 0x69, 0xf6, 0x09, 0x31, 0xdb, 0x50, 0x15, 0x45, - 0xbd, 0x9f, 0x28, 0xbd, 0x1f, 0x14, 0x84, 0x56, 0xc5, 0x55, 0x06, 0xbb, 0x52, 0x4b, 0x83, 0xc6, - 0x24, 0x7c, 0x81, 0xa1, 0x30, 0xde, 0x53, 0xdf, 0x27, 0xe9, 0x04, 0xbe, 0xf6, 0x1e, 0x00, 0x7c, - 0x8e, 0xc5, 0x72, 0x03, 0x31, 0x0c, 0x86, 0xd5, 0xd3, 0xf6, 0x54, 0x66, 0xe6, 0x6b, 0xf8, 0x0d, - 0xca, 0x8c, 0x67, 0xcd, 0x82, 0x92, 0x78, 0x6a, 0x5b, 0x19, 0x6b, 0x83, 0x4f, 0x5f, 0xa5, 0xcc, - 0x66, 0xfc, 0xbf, 0x0f, 0x1c, 0x49, 0xd3, 0xb3, 0xe5, 0x46, 0x1f, 0xbb, 0xa5, 0x32, 0x0a, 0x79, - 0xe1, 0x00, 0xe3, 0x39, 0x05, 0x67, 0x7c, 0x6c, 0x31, 0x6d, 0xc6, 0x39, 0xc0, 0x85, 0xf1, 0xd4, - 0x15, 0x72, 0x06, 0x1d, 0x95, 0x6b, 0x56, 0x8a, 0x25, 0x0c, 0xd4, 0xa2, 0x58, 0x9f, 0xc1, 0x32, - 0x5a, 0x0e, 0x31, 0x72, 0x1e, 0xa3, 0x67, 0xa4, 0x9e, 0x9e, 0xe9, 0xc7, 0xd7, 0x24, 0x81, 0x91, - 0xb3, 0x03, 0x94, 0x0a, 0xa6, 0xec, 0x5a, 0xb1, 0x37, 0xec, 0x31, 0xc4, 0x99, 0x61, 0x4c, 0x2c, - 0xc1, 0x7c, 0x5b, 0x12, 0x98, 0xef, 0x9a, 0xba, 0x81, 0xe8, 0x3d, 0x6e, 0x1c, 0x93, 0x60, 0xb2, - 0x06, 0x21, 0x49, 0xcb, 0x73, 0x17, 0xe6, 0x83, 0x94, 0x2b, 0x65, 0x18, 0x65, 0x76, 0x9a, 0x19, - 0x3a, 0x14, 0x20, 0x7a, 0xb5, 0x8c, 0xbe, 0x59, 0x5e, 0xc6, 0x2a, 0x70, 0x54, 0xc6, 0x07, 0x9b, - 0x87, 0xf8, 0x43, 0x10, 0xbc, 0x49, 0x14, 0x87, 0x92, 0x06, 0x22, 0xaf, 0x26, 0xde, 0x53, 0x9a, - 0xff, 0x66, 0x84, 0xb0, 0x57, 0x6f, 0x5b, 0xab, 0x93, 0x9a, 0xe9, 0x38, 0x54, 0x43, 0x98, 0x7b, - 0x34, 0x22, 0xe8, 0x38, 0x23, 0xa7, 0x9b, 0xb7, 0x58, 0x84, 0xf5, 0x2f, 0xa2, 0x08, 0xf3, 0x1f, - 0x8a, 0xfa, 0xee, 0x8b, 0x1b, 0x42, 0x34, 0xa0, 0xc0, 0xff, 0xb3, 0x77, 0x86, 0x6c, 0x9d, 0x94, - 0xad, 0xe3, 0x0b, 0x7b, 0xe6, 0x03, 0xb7, 0xfa, 0x0d, 0x37, 0xfb, 0x19, 0x37, 0xf3, 0x0d, 0x07, - 0xd7, 0x81, 0x2f, 0x4c, 0xfa, 0xb4, 0xa2, 0x7c, 0xac, 0x01, 0x2f, 0x09, 0x0a, 0x78, 0x00, 0x7c, - 0x90, 0x45, 0x76, 0xc4, 0x40, 0x0c, 0x44, 0x15, 0x66, 0xe6, 0x53, 0x98, 0xbd, 0xcf, 0x36, 0x9c, - 0xb9, 0x80, 0x9e, 0xd9, 0x6d, 0x92, 0x47, 0xdd, 0x03, 0xb7, 0x1f, 0x66, 0x5a, 0x0a, 0xab, 0xea, - 0xc3, 0x1f, 0xd3, 0xb7, 0x08, 0x98, 0x30, 0x17, 0x52, 0x62, 0x49, 0x61, 0x54, 0xc2, 0x19, 0x47, - 0x75, 0xe4, 0xa9, 0x88, 0xe1, 0x0e, 0x7d, 0x16, 0x61, 0x12, 0x21, 0x4b, 0xd3, 0x32, 0x51, 0x46, - 0x2c, 0xbc, 0xc2, 0x80, 0xfb, 0x82, 0xd8, 0x43, 0x52, 0x1e, 0x56, 0x84, 0x51, 0x77, 0xb0, 0x59, - 0x79, 0xc5, 0x60, 0x5a, 0x49, 0x62, 0x09, 0xe7, 0x44, 0xe5, 0x70, 0xb5, 0x1d, 0x31, 0x9c, 0x8d, - 0x9b, 0x70, 0x36, 0xd8, 0x29, 0xc5, 0x70, 0x2b, 0x48, 0x3d, 0x05, 0xd0, 0x30, 0x7e, 0x22, 0xc2, - 0x80, 0xca, 0xda, 0xab, 0x04, 0x55, 0xc8, 0x5e, 0x28, 0x08, 0xfd, 0x22, 0x82, 0xe7, 0x96, 0xf4, - 0xe1, 0x78, 0xc1, 0xcd, 0xf1, 0xcc, 0xcd, 0x35, 0xee, 0xd4, 0x3d, 0x59, 0xd0, 0x3d, 0x9e, 0xe8, - 0x1e, 0x2f, 0xeb, 0xee, 0xbd, 0x7f, 0xa2, 0x65, 0x18, 0xe6, 0x46, 0xe5, 0xb7, 0xb8, 0x55, 0x14, - 0xf0, 0x36, 0xd4, 0x7f, 0xeb, 0x88, 0x58, 0xc0, 0xd9, 0x6e, 0x26, 0x91, 0xac, 0x2b, 0xea, 0xc0, - 0xdb, 0x88, 0x0d, 0xdc, 0x93, 0x3f, 0xd6, 0xc6, 0xb2, 0xa9, 0xd4, 0x12, 0x84, 0x47, 0x5c, 0x9e, - 0x6d, 0x81, 0x72, 0x64, 0xeb, 0x06, 0x4e, 0x65, 0x76, 0x47, 0xbd, 0x67, 0x2f, 0x37, 0x2c, 0x4b, - 0x37, 0xb1, 0x63, 0x3a, 0x4d, 0xc3, 0x9d, 0x6d, 0xc3, 0x7d, 0xab, 0x1a, 0x58, 0x8f, 0xc2, 0x79, - 0xe7, 0xd0, 0x76, 0xad, 0xfe, 0x0c, 0xb1, 0x7a, 0x18, 0x00, 0x7d, 0x50, 0x35, 0x77, 0x03, 0x31, - 0x0c, 0x56, 0xb9, 0x9d, 0xca, 0x34, 0x95, 0xb7, 0x52, 0x70, 0x2e, 0x33, 0xe3, 0xa8, 0xa7, 0xdc, - 0x29, 0x89, 0x5f, 0x7c, 0xd6, 0x55, 0x76, 0xe0, 0xdf, 0x97, 0x99, 0x46, 0xc1, 0x87, 0x70, 0xb1, - 0x7e, 0x88, 0xf9, 0x62, 0x2e, 0x83, 0x91, 0x24, 0x29, 0x39, 0x23, 0x0e, 0x95, 0x62, 0x23, 0xc8, - 0xa1, 0xca, 0xea, 0x38, 0xc0, 0x64, 0xb9, 0x6e, 0x2d, 0x4c, 0xd6, 0x7d, 0x09, 0xfa, 0x6a, 0xc6, - 0x7b, 0x4c, 0x24, 0xe6, 0x04, 0xfa, 0x94, 0x53, 0xa6, 0xc0, 0x8a, 0x25, 0x35, 0x71, 0x85, 0x51, - 0x7d, 0x36, 0x97, 0x45, 0xcf, 0x6a, 0xc8, 0x66, 0xd0, 0x8a, 0x12, 0x4a, 0x20, 0x74, 0x82, 0xdc, - 0x0a, 0xac, 0x8e, 0x2c, 0x7a, 0x76, 0x5e, 0xd4, 0xc3, 0x42, 0xf6, 0xff, 0xfb, 0xd0, 0xff, 0x57, - 0xa5, 0x5a, 0xa6, 0xb0, 0xba, 0x9a, 0x41, 0x6f, 0x25, 0x60, 0xf4, 0xce, 0x05, 0x43, 0x75, 0x67, - 0xca, 0x86, 0xe3, 0x4f, 0x9b, 0x66, 0xb6, 0x70, 0x97, 0x29, 0x7e, 0x2c, 0xfa, 0x02, 0x6b, 0x62, - 0x9e, 0xe8, 0xa2, 0x2a, 0x05, 0xe8, 0xbc, 0x35, 0xfe, 0x98, 0x7d, 0x15, 0xaf, 0xa8, 0xf2, 0xa3, - 0x82, 0x92, 0x65, 0x98, 0x7e, 0xca, 0x3d, 0xf0, 0x29, 0xf7, 0xc0, 0x5b, 0x6e, 0x98, 0xf8, 0x3f, - 0xc4, 0xa0, 0x48, 0x82, 0x9e, 0xb5, 0xc1, 0x0a, 0x03, 0x2f, 0x4b, 0x80, 0x4b, 0x76, 0x9e, 0x37, - 0x29, 0xc5, 0x4b, 0xb1, 0xa4, 0xf8, 0xce, 0xd5, 0xbb, 0xef, 0x62, 0x13, 0x51, 0x10, 0x5d, 0xda, - 0xbe, 0x3c, 0x3b, 0x91, 0xe6, 0xb3, 0x99, 0x57, 0x1f, 0x4f, 0x16, 0x86, 0x8e, 0xe4, 0x82, 0x96, - 0x4e, 0x32, 0xc5, 0x8b, 0xec, 0xb7, 0x13, 0x36, 0x4d, 0xd9, 0xc0, 0xd8, 0x77, 0xc0, 0xc2, 0xcb, - 0xfe, 0x01, 0xb5, 0x8f, 0xb5, 0x95, 0xc4, 0x60, 0x0c, 0x84, 0x15, 0x1d, 0x33, 0x33, 0x54, 0x60, - 0x6a, 0xe1, 0x20, 0x3c, 0x2a, 0x40, 0xcf, 0xa0, 0xf5, 0x93, 0x49, 0xfe, 0x61, 0xa1, 0xfb, 0x65, - 0x5e, 0xa7, 0x97, 0x89, 0x66, 0xe6, 0x13, 0x18, 0x29, 0x42, 0xdd, 0xe3, 0x50, 0x30, 0x70, 0xff, - 0x30, 0x96, 0xb2, 0x0e, 0x2b, 0x96, 0x0a, 0x75, 0x98, 0xb0, 0x60, 0x54, 0x10, 0x3c, 0xb5, 0x4d, - 0x04, 0x47, 0x39, 0x1b, 0x83, 0xa5, 0x24, 0x54, 0xc2, 0x91, 0xa6, 0x9a, 0x42, 0x4b, 0x1a, 0xae, - 0x30, 0xd2, 0x9c, 0xa4, 0x84, 0xda, 0xf8, 0x81, 0x0f, 0xc7, 0x22, 0x25, 0x1a, 0xd2, 0x9d, 0xe1, - 0xea, 0x16, 0x0b, 0xd1, 0x21, 0x8a, 0x0d, 0x61, 0x3f, 0xf8, 0x27, 0x63, 0xf8, 0xb3, 0xa1, 0x1d, - 0x79, 0x7e, 0xfa, 0x58, 0x48, 0x9a, 0x72, 0x95, 0xe2, 0xdc, 0xe2, 0x7e, 0x66, 0x41, 0xa6, 0xae, - 0xa4, 0x0b, 0xfb, 0xf3, 0xcd, 0xf9, 0xda, 0x66, 0x37, 0xc0, 0xf7, 0xbf, 0xef, 0xc0, 0x6f, 0x84, - 0x7c, 0x6d, 0xb5, 0x8b, 0x02, 0x5e, 0x47, 0xa8, 0xaf, 0x5d, 0x6e, 0x31, 0x9c, 0x2e, 0x01, 0x9f, - 0x6e, 0xa4, 0x9e, 0x6a, 0x7f, 0xd1, 0x3c, 0xa0, 0x44, 0x13, 0x6a, 0x2c, 0x95, 0xb5, 0x70, 0xb9, - 0xf4, 0xc6, 0xf3, 0xda, 0xee, 0x6c, 0x26, 0xba, 0x5d, 0x5f, 0xcc, 0x45, 0x9b, 0x86, 0x60, 0x3d, - 0xd7, 0x75, 0xa9, 0x11, 0xfc, 0x7a, 0x84, 0xbc, 0xbd, 0x04, 0xbb, 0x3d, 0xe7, 0x1a, 0x00, 0x54, - 0x4e, 0x85, 0x4d, 0x45, 0x41, 0x10, 0xdc, 0x28, 0xb4, 0xf0, 0x88, 0xe1, 0xee, 0x56, 0x00, 0x25, - 0xe0, 0xd1, 0xc9, 0xc9, 0x42, 0x9e, 0xdc, 0xed, 0xd9, 0x97, 0xee, 0xbf, 0x6b, 0x7c, 0x7c, 0xa8, - 0x3c, 0xdc, 0xdf, 0xdf, 0x33, 0x12, 0x07, 0x56, 0x85, 0x13, 0x3a, 0x49, 0x0a, 0x52, 0x14, 0xbc, - 0x80, 0x87, 0x13, 0xc6, 0xab, 0x0e, 0x99, 0x7d, 0x96, 0x94, 0x69, 0x57, 0xc4, 0x21, 0x73, 0xea, - 0x73, 0xa2, 0x9d, 0x39, 0x49, 0xf4, 0x89, 0x37, 0x56, 0x0e, 0x1f, 0x4f, 0xf8, 0xe8, 0x85, 0xc4, - 0x8e, 0xf1, 0xf1, 0xfb, 0xf0, 0xf8, 0xfa, 0x08, 0x23, 0x2e, 0x28, 0x5f, 0x8b, 0x47, 0x52, 0xb6, - 0x16, 0xe8, 0x8e, 0xe9, 0x1a, 0x21, 0xd3, 0xc5, 0xa0, 0xfe, 0xab, 0xa9, 0x6a, 0xeb, 0x9c, 0xe1, - 0xc4, 0xb2, 0xa3, 0x6a, 0xb9, 0x4f, 0x07, 0xd0, 0xa9, 0xb6, 0xff, 0x0c, 0xce, 0xc1, 0xcb, 0x80, - 0x0e, 0x45, 0xcf, 0x07, 0xe1, 0x62, 0x29, 0xb4, 0xb7, 0xf1, 0xe0, 0x18, 0xdb, 0xda, 0x78, 0x8c, - 0x49, 0x40, 0x77, 0xe0, 0xb3, 0x5c, 0x01, 0x64, 0x39, 0x2f, 0x92, 0xb5, 0xbc, 0x70, 0x6b, 0x45, - 0xd1, 0xcc, 0xe2, 0x43, 0xb2, 0x47, 0x02, 0x4d, 0x8e, 0xcb, 0xdc, 0x8c, 0x96, 0xc6, 0x33, 0x02, - 0xc3, 0xb5, 0x24, 0xb1, 0x28, 0xde, 0xd9, 0x38, 0xbe, 0xb8, 0x02, 0xec, 0x17, 0x24, 0x23, 0x85, - 0xe0, 0x46, 0x4a, 0xa1, 0x05, 0x94, 0x80, 0x05, 0x71, 0xea, 0x00, 0xb5, 0x51, 0x45, 0x56, 0x04, - 0x51, 0x0c, 0xcc, 0x1a, 0xdb, 0xcd, 0x0e, 0x3b, 0x02, 0xce, 0x25, 0x70, 0xd8, 0xe7, 0xb5, 0xd4, - 0xd0, 0x69, 0x49, 0xfa, 0x25, 0x7f, 0xe4, 0xf6, 0xb8, 0xbb, 0xee, 0xcb, 0x8b, 0xce, 0x2a, 0xd1, - 0xe6, 0xc0, 0x4a, 0xf0, 0xfe, 0x3a, 0x17, 0xd6, 0xf5, 0x99, 0x8a, 0x29, 0x7b, 0x56, 0x8a, 0x71, - 0xde, 0x82, 0x96, 0x46, 0x91, 0xd3, 0x7c, 0x23, 0x11, 0xdc, 0x59, 0x89, 0x8e, 0xe6, 0x1d, 0x3d, - 0xb2, 0x04, 0xa7, 0x05, 0xb3, 0x8e, 0x03, 0x3e, 0x86, 0xd3, 0xec, 0xc6, 0xdb, 0xec, 0xe1, 0xa8, - 0x6d, 0x69, 0x29, 0xe0, 0x92, 0xb5, 0xb7, 0x5a, 0x4b, 0x13, 0x19, 0xca, 0x73, 0xc5, 0x93, 0x7b, - 0xc5, 0x01, 0xe7, 0x2e, 0xe5, 0x39, 0x18, 0xd1, 0xab, 0x4d, 0x68, 0xde, 0x9f, 0x58, 0xcc, 0x27, - 0x78, 0x27, 0x9a, 0xb5, 0x5c, 0x54, 0x59, 0xa2, 0xd9, 0xcd, 0xef, 0xe6, 0x1d, 0x70, 0x6b, 0x9e, - 0xb1, 0xa5, 0x8c, 0xd5, 0x18, 0xd3, 0x04, 0xbf, 0x56, 0x0b, 0x68, 0x98, 0xc7, 0xf3, 0x3a, 0xdb, - 0x6f, 0x8a, 0xaf, 0x72, 0x1f, 0xff, 0xda, 0x69, 0xe7, 0x57, 0x1f, 0xec, 0xbe, 0xcd, 0x46, 0xaa, - 0xe0, 0x8a, 0x74, 0x9f, 0xfc, 0x31, 0xf1, 0x25, 0x62, 0x08, 0xc9, 0xc8, 0xcc, 0xcb, 0xf6, 0xcb, - 0x4f, 0x01, 0xb4, 0x41, 0x0d, 0xb8, 0x15, 0x05, 0x51, 0xf4, 0x06, 0xb5, 0xed, 0x35, 0xd4, 0x8a, - 0x53, 0x33, 0xaa, 0xa3, 0x93, 0x67, 0xcf, 0x7d, 0x83, 0x8f, 0xe5, 0xf7, 0x9b, 0x63, 0x1f, 0x05, - 0x78, 0xbf, 0x44, 0x96, 0x68, 0x8d, 0xa2, 0xb1, 0x29, 0x68, 0x41, 0x05, 0x65, 0xe0, 0x98, 0x40, - 0xd1, 0x06, 0xd8, 0xd5, 0x81, 0xaa, 0x06, 0x0a, 0x85, 0x34, 0x86, 0x16, 0x99, 0x0b, 0xb4, 0x0f, - 0x68, 0x7b, 0xe4, 0x6e, 0xee, 0x1a, 0x1e, 0x17, 0xa5, 0x23, 0x12, 0x16, 0x50, 0x8e, 0x9f, 0x30, - 0xdc, 0x3c, 0x00, 0x1d, 0x54, 0xb4, 0xdb, 0x18, 0x6b, 0x49, 0x98, 0x80, 0x16, 0x6c, 0x34, 0x18, - 0xc4, 0x05, 0xd1, 0xcf, 0xd3, 0xdf, 0xc9, 0xd1, 0xc9, 0x39, 0x5c, 0xc7, 0x8f, 0x02, 0x3b, 0xe8, - 0x62, 0xff, 0x2f, 0xd5, 0x1a, 0x87, 0xb5, 0xe2, 0xf8, 0xf8, 0xf8, 0x08, 0x3d, 0xe1, 0xf4, 0x93, - 0xf0, 0xa7, 0x72, 0xbc, 0x2c, 0x50, 0x6f, 0xc7, 0x36, 0x88, 0x96, 0xac, 0x86, 0x1c, 0xbb, 0xeb, - 0x9c, 0x95, 0x03, 0x36, 0x0e, 0x04, 0x23, 0xa8, 0x37, 0xce, 0x84, 0x93, 0x43, 0x07, 0x42, 0xb3, - 0xd2, 0x43, 0x21, 0xcc, 0xdf, 0x37, 0x22, 0xb4, 0xe2, 0xaf, 0x0c, 0x40, 0xd3, 0x6f, 0xe2, 0x30, - 0x6e, 0x4f, 0xad, 0x0f, 0xf7, 0x50, 0x6a, 0xda, 0x6e, 0xaa, 0xd9, 0x6e, 0x66, 0x44, 0xd3, 0x03, - 0x1f, 0xa7, 0x7b, 0x9a, 0x66, 0x06, 0x48, 0x27, 0x2f, 0xec, 0xa1, 0x58, 0x7f, 0x82, 0xfe, 0x01, - 0x6d, 0x8e, 0x05, 0x72, 0x03, 0x31, 0x0c, 0x00, 0x55, 0x0e, 0x94, 0x99, 0xfb, 0x86, 0xd2, 0x23, - 0x3a, 0x50, 0xa6, 0x41, 0xcd, 0xf1, 0xe9, 0x48, 0x8e, 0xe4, 0xc0, 0xef, 0xc3, 0x9c, 0x0c, 0x98, - 0xbd, 0xbb, 0xf0, 0x4f, 0x0e, 0x63, 0x21, 0xe1, 0xf3, 0x03, 0x7a, 0x9c, 0x1b, 0xa7, 0x20, 0x2e, - 0x50, 0x1c, 0x9f, 0x18, 0xdd, 0x2c, 0x80, 0xab, 0xba, 0xba, 0x50, 0x4d, 0x49, 0x15, 0x73, 0xf6, - 0x83, 0x1c, 0xaa, 0x12, 0x98, 0xc0, 0xb1, 0x81, 0xc0, 0x11, 0xba, 0x42, 0x7e, 0x14, 0xa0, 0xe8, - 0xc3, 0xe3, 0x03, 0x6c, 0x33, 0xe7, 0xa8, 0x81, 0x34, 0x02, 0x81, 0x8d, 0xaf, 0xc7, 0xe5, 0x34, - 0x34, 0x0a, 0xe7, 0x3d, 0x64, 0x69, 0x0a, 0x59, 0x1a, 0x23, 0xf7, 0x47, 0xc8, 0x40, 0x4d, 0xc1, - 0x4d, 0x38, 0x60, 0x77, 0x40, 0xc4, 0xbc, 0x66, 0x2d, 0x94, 0xa7, 0x14, 0x47, 0x38, 0x7b, 0x57, - 0xd2, 0xa0, 0x50, 0x16, 0x28, 0xfd, 0xbd, 0xbc, 0x2d, 0x75, 0xdf, 0xf5, 0xdc, 0xd7, 0x61, 0x3d, - 0xcb, 0xe0, 0x5a, 0x03, 0x21, 0xa7, 0x3b, 0xf7, 0x4a, 0xae, 0x9b, 0x14, 0x12, 0x6c, 0x4f, 0xf5, - 0x6c, 0x1b, 0x47, 0x2c, 0xd9, 0xde, 0xf7, 0xa0, 0xd5, 0xe5, 0xf8, 0x5d, 0xdd, 0xf6, 0xb8, 0xf1, - 0x78, 0xae, 0xf1, 0x64, 0xb6, 0x63, 0x67, 0xaa, 0xf1, 0x6c, 0xae, 0x71, 0x7b, 0xd8, 0xb8, 0xdd, - 0xe5, 0xe4, 0x54, 0x38, 0x19, 0x7a, 0xb1, 0x63, 0xa1, 0x03, + 0x7d, 0x51, 0x45, 0x76, 0x03, 0x31, 0x0c, 0xfd, 0xbb, 0xd2, 0xaa, 0xcc, 0xb8, 0xed, 0x2a, 0x70, + 0x88, 0xde, 0x42, 0x43, 0x72, 0xe3, 0x17, 0xb3, 0x95, 0xc9, 0xf1, 0x0b, 0x61, 0x36, 0xa3, 0xf4, + 0x01, 0x03, 0x36, 0xc2, 0x35, 0x49, 0x2a, 0xeb, 0x21, 0x27, 0x6a, 0xbb, 0x54, 0x7b, 0x1b, 0x4a, + 0xa7, 0xbd, 0xa3, 0x54, 0x36, 0xda, 0xd3, 0x58, 0x2b, 0x4d, 0x8a, 0xad, 0x57, 0x0a, 0x9f, 0x0e, + 0x17, 0x43, 0x9d, 0x33, 0x59, 0xdf, 0xb0, 0xc5, 0x45, 0xe2, 0xc0, 0xa5, 0x70, 0xc2, 0x03, 0x55, + 0x49, 0x37, 0xdf, 0x4c, 0x9c, 0x83, 0xf3, 0x63, 0x7c, 0xa4, 0xdc, 0xed, 0x75, 0xf1, 0xe8, 0xab, + 0xcc, 0xa9, 0xe5, 0x44, 0x36, 0x8a, 0xe0, 0xd2, 0x7b, 0x4b, 0xd3, 0x03, 0x3c, 0xd3, 0xfa, 0xdd, + 0x45, 0x66, 0x97, 0x7d, 0xc2, 0x85, 0x70, 0xb2, 0xda, 0x95, 0x86, 0xea, 0x41, 0x29, 0xc0, 0xd7, + 0x14, 0xe0, 0x0e, 0x60, 0x95, 0x61, 0x3c, 0x53, 0xc8, 0x78, 0x18, 0xe5, 0x0a, 0x0f, 0xff, 0x38, + 0x71, 0xba, 0x82, 0xee, 0x74, 0x81, 0xee, 0x6c, 0x25, 0xef, 0xe9, 0x7a, 0xfc, 0xe3, 0xde, 0x81, + 0xb8, 0x6a, 0x64, 0xcc, 0x6a, 0xf4, 0x97, 0x29, 0xd4, 0xdd, 0x49, 0x2e, 0x36, 0x24, 0x78, 0x98, + 0x4a, 0x70, 0xb9, 0x4e, 0xf3, 0x8d, 0xb2, 0xb6, 0x6b, 0x80, 0xae, 0x37, 0x84, 0x38, 0x9d, 0x0a, + 0xb1, 0x09, 0x74, 0xee, 0x54, 0xdb, 0xa7, 0xee, 0xff, 0xb0, 0x8d, 0xfb, 0x1f, 0xed, 0xc4, 0x28, + 0x87, 0xd3, 0x9d, 0xac, 0xe6, 0x2e, 0xbe, 0x85, 0x3c, 0x5f, 0xfe, 0x02, 0x85, 0x4f, 0xc3, 0x76, + 0x44, 0x41, 0x10, 0xbd, 0xdb, 0xd1, 0x36, 0x36, 0x36, 0xb1, 0x93, 0xbf, 0xa9, 0x79, 0xa8, 0x3e, + 0xe9, 0x87, 0xf6, 0xe0, 0xef, 0xc7, 0xe6, 0xaa, 0x78, 0x85, 0x7f, 0x2e, 0x02, 0x27, 0xd4, 0xfe, + 0xa2, 0x4f, 0x4a, 0x74, 0x69, 0x22, 0x25, 0xb5, 0x22, 0x17, 0xa5, 0x52, 0x93, 0x68, 0x15, 0x05, + 0x09, 0x2e, 0xb5, 0x10, 0x38, 0x6b, 0xf9, 0x78, 0xd6, 0x77, 0xa4, 0x90, 0xd3, 0xe1, 0xba, 0xb4, + 0x21, 0x4c, 0x87, 0x3a, 0x77, 0x4d, 0xa4, 0xbc, 0xd4, 0x2a, 0x97, 0x81, 0x82, 0x08, 0x6b, 0x9c, + 0x71, 0xc1, 0xb3, 0xe7, 0x3d, 0xf7, 0xab, 0x45, 0xfd, 0xab, 0x05, 0xfd, 0xab, 0x45, 0xfd, 0xea, + 0x06, 0x06, 0x9c, 0x4f, 0xaf, 0x37, 0x0a, 0x17, 0xc6, 0x8f, 0x26, 0x85, 0x93, 0x01, 0xc7, 0xe4, + 0x34, 0x5f, 0x0b, 0x81, 0xfb, 0xc1, 0x69, 0x46, 0xa9, 0x70, 0x39, 0x4b, 0xa4, 0x50, 0x5d, 0x0a, + 0xe4, 0xd8, 0x70, 0x14, 0xd8, 0xe1, 0x92, 0x74, 0xec, 0xd9, 0xb5, 0xd9, 0xd1, 0xf0, 0x19, 0x47, + 0x5a, 0x97, 0x34, 0x59, 0x2c, 0x5f, 0x0e, 0x77, 0x85, 0x54, 0x78, 0x35, 0x1e, 0x8f, 0xb3, 0x9c, + 0x0a, 0xd5, 0x5c, 0x7a, 0x4f, 0xa5, 0x4e, 0xb9, 0x44, 0xd5, 0x44, 0x2e, 0xc8, 0x30, 0x84, 0x8d, + 0x5c, 0xa4, 0xec, 0x50, 0x75, 0x7f, 0x9b, 0xd8, 0x70, 0x45, 0xc6, 0xe3, 0x7c, 0x48, 0x84, 0xf3, + 0x51, 0xca, 0x61, 0x1e, 0x34, 0x16, 0xd8, 0x1a, 0x33, 0xf3, 0x7d, 0x75, 0x50, 0xc5, 0x95, 0x04, + 0x41, 0x08, 0xe5, 0xba, 0xee, 0xee, 0x31, 0xac, 0x66, 0xc3, 0x2b, 0xa1, 0x67, 0x68, 0xa3, 0x84, + 0xb5, 0xe8, 0xc7, 0x5d, 0x4e, 0x38, 0x5f, 0xa0, 0x4b, 0xb5, 0x92, 0xc3, 0x9f, 0x37, 0x4c, 0x5f, + 0x98, 0x28, 0x90, 0x51, 0x4a, 0x28, 0x36, 0x53, 0xfa, 0x19, 0x24, 0x4d, 0x54, 0x85, 0x43, 0x91, + 0x06, 0xc7, 0x0d, 0xb8, 0x5e, 0x99, 0x1d, 0x64, 0x6a, 0xb3, 0x24, 0x38, 0x18, 0x9c, 0x35, 0xdc, + 0x9a, 0x1a, 0x5d, 0xd7, 0x0c, 0x4e, 0x0a, 0x45, 0x27, 0x4d, 0x30, 0x2d, 0x4b, 0x8b, 0xc9, 0x78, + 0x16, 0xb4, 0x35, 0xc1, 0x4b, 0xf1, 0x5d, 0xd7, 0xf0, 0xf2, 0x9d, 0x2d, 0xbc, 0xfc, 0x72, 0xc1, + 0x70, 0x52, 0x71, 0xce, 0xd8, 0x88, 0xa7, 0x06, 0x4e, 0x82, 0x24, 0x4d, 0x86, 0x75, 0x1b, 0x91, + 0xbb, 0x39, 0x91, 0x95, 0xc9, 0xc9, 0xf4, 0x02, 0x6e, 0x56, 0x26, 0xa7, 0x0b, 0xe4, 0xef, 0x57, + 0xaf, 0x26, 0xe4, 0x4f, 0x96, 0xc9, 0xef, 0x4d, 0x7f, 0x9d, 0xa2, 0x4d, 0xec, 0x3b, 0x84, 0x94, + 0x43, 0x2b, 0xbf, 0x70, 0x56, 0x53, 0xc7, 0xb8, 0x7f, 0x0c, 0x26, 0x29, 0xeb, 0x50, 0x99, 0x52, + 0x56, 0x38, 0x5b, 0xfe, 0xba, 0xb7, 0x80, 0x78, 0xb1, 0x82, 0xb8, 0x3b, 0x41, 0xdc, 0x5d, 0x41, + 0xdc, 0x6c, 0x17, 0x16, 0xd4, 0x48, 0x0b, 0x8f, 0x43, 0xd3, 0xa6, 0xf9, 0xc0, 0xba, 0x69, 0x3a, + 0x34, 0x70, 0x92, 0x1f, 0xce, 0xf4, 0x5f, 0xae, 0x70, 0x3e, 0x5a, 0x60, 0x73, 0x38, 0x41, 0xef, + 0x03, 0x75, 0xce, 0x83, 0x72, 0x04, 0x41, 0x10, 0x06, 0xe0, 0x2e, 0xc6, 0xb6, 0x6d, 0x3b, 0x79, + 0x82, 0xd8, 0x2a, 0x77, 0x0d, 0x7a, 0xb3, 0xb3, 0x9a, 0xcd, 0xcc, 0xe4, 0xee, 0xf5, 0x73, 0xf6, + 0x95, 0x9b, 0xdf, 0x0f, 0x3e, 0x45, 0x8e, 0x04, 0x66, 0x4e, 0xd1, 0x79, 0x0e, 0x1d, 0x99, 0x58, + 0x25, 0x2c, 0x42, 0xe1, 0x33, 0x07, 0x70, 0x53, 0x1c, 0x7e, 0x2b, 0x43, 0x11, 0x59, 0x8b, 0x2f, + 0x2c, 0x25, 0x83, 0x42, 0xc7, 0x29, 0x4b, 0x94, 0x4e, 0xd0, 0x30, 0xa9, 0x34, 0xf2, 0x88, 0x60, + 0x0b, 0x53, 0x0b, 0xeb, 0x7f, 0x96, 0xc3, 0x78, 0xa8, 0x72, 0x9b, 0xb1, 0x96, 0x14, 0xc3, 0xb8, + 0xa1, 0x94, 0x58, 0xee, 0x29, 0xcc, 0x23, 0x37, 0x4a, 0xfe, 0x10, 0x92, 0x4d, 0x13, 0x9d, 0x85, + 0x05, 0xcd, 0x2d, 0x99, 0x0c, 0x19, 0x8c, 0x7f, 0x9d, 0x83, 0x09, 0xad, 0x63, 0x2c, 0x36, 0x60, + 0x09, 0xeb, 0x67, 0x83, 0x1f, 0x86, 0x89, 0xb0, 0x8d, 0xbb, 0x93, 0x77, 0x37, 0xf3, 0xee, 0x64, + 0x8d, 0x3b, 0x59, 0x71, 0x17, 0x1b, 0xdc, 0xa5, 0xfa, 0xdf, 0x53, 0x35, 0xee, 0x4a, 0x83, 0xdb, + 0xf9, 0xfe, 0x70, 0xd2, 0xd2, 0x5c, 0xc8, 0x9b, 0xb3, 0x79, 0x73, 0x36, 0xab, 0x3c, 0x05, 0xbd, + 0x15, 0x6d, 0xb4, 0x41, 0x9b, 0x35, 0xf6, 0xf8, 0xe4, 0x18, 0xc6, 0xea, 0x1f, 0xf7, 0xd5, 0xa0, + 0x13, 0x0d, 0x68, 0xaf, 0xa5, 0xc4, 0x6a, 0x03, 0xa0, 0x42, 0x1d, 0x32, 0xf4, 0x59, 0x22, 0x7d, + 0x8a, 0x24, 0x26, 0xc6, 0x43, 0x3a, 0x3e, 0xc6, 0x93, 0x43, 0xc9, 0x63, 0x3c, 0xbc, 0x3a, 0x6f, + 0x99, 0xec, 0x28, 0x1f, 0x6a, 0xc9, 0x68, 0xc7, 0x1c, 0xc9, 0x96, 0x1b, 0x57, 0xf9, 0x8d, 0xb9, + 0x4a, 0xe0, 0x7f, 0xa5, 0xcf, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x85, 0x61, 0x4f, 0x5d, 0x01, 0xa1, + 0x96, 0x13, 0xc0, 0x5c, 0x92, 0x63, 0xc0, 0xc8, 0x6e, 0xa5, 0x8d, 0x51, 0xa3, 0x34, 0x76, 0xe5, + 0x14, 0xce, 0x8f, 0xca, 0x50, 0x95, 0x89, 0x21, 0xfb, 0xef, 0x4f, 0x7e, 0x10, 0xa2, 0x44, 0x87, + 0x83, 0x63, 0x3f, 0xd0, 0xe8, 0x91, 0xf5, 0x89, 0x64, 0x0c, 0xda, 0xd6, 0x77, 0x09, 0x55, 0x24, + 0x61, 0x26, 0x7d, 0x93, 0xc2, 0x31, 0x86, 0x9c, 0x31, 0x89, 0xa7, 0x04, 0x15, 0x3b, 0x96, 0x6f, + 0x6b, 0xcc, 0xd2, 0xf5, 0x92, 0x26, 0xc7, 0x41, 0x18, 0xd5, 0xf9, 0x20, 0xd8, 0x8d, 0x04, 0xe7, + 0x57, 0xee, 0xa0, 0xde, 0x1c, 0xd5, 0x4a, 0x13, 0xb9, 0x99, 0x14, 0x9a, 0x0d, 0xbb, 0xb3, 0x6d, + 0xa1, 0xb0, 0xb7, 0x85, 0x40, 0x95, 0xe7, 0xd0, 0xc7, 0x3f, 0x73, 0x2e, 0x8b, 0xd5, 0x6c, 0xac, + 0x66, 0xb5, 0x4e, 0x3f, 0xcf, 0xb4, 0x85, 0xc2, 0xc1, 0x16, 0x02, 0x70, 0x23, 0x9a, 0x66, 0x22, + 0xbe, 0x3f, 0xae, 0xb8, 0x16, 0x1f, 0x8c, 0x8e, 0x35, 0x57, 0x04, 0x51, 0x0c, 0x85, 0xd3, 0xe1, + 0x4e, 0x85, 0x7b, 0x85, 0x6b, 0x8f, 0x5b, 0xc7, 0xf6, 0xf7, 0x8c, 0xe4, 0xb0, 0x6f, 0x24, 0x79, + 0x86, 0xfd, 0x7a, 0xdc, 0xbd, 0xbd, 0xfa, 0xd1, 0x09, 0xb3, 0x8d, 0xcc, 0x72, 0xda, 0x58, 0x81, + 0x67, 0xcb, 0x49, 0x64, 0x8f, 0x4a, 0x7d, 0x02, 0x8d, 0x09, 0x44, 0xc1, 0xd7, 0x91, 0xbd, 0x24, + 0x15, 0x02, 0x4b, 0x50, 0x1f, 0x88, 0x4e, 0x4d, 0x75, 0x73, 0xa0, 0x68, 0x2c, 0xec, 0x65, 0x4d, + 0x5d, 0x38, 0x35, 0x91, 0x91, 0x69, 0x6d, 0x13, 0x31, 0x2a, 0xf0, 0x49, 0x6e, 0x14, 0x69, 0xc5, + 0x34, 0x7d, 0x11, 0x52, 0x1a, 0x28, 0x4d, 0x08, 0xa8, 0x35, 0xe7, 0x9a, 0x06, 0x44, 0x05, 0xa1, + 0xc9, 0x55, 0xf5, 0x6d, 0x7e, 0xe1, 0x31, 0xff, 0xc2, 0x40, 0x83, 0xaa, 0x35, 0x02, 0xfb, 0x4b, + 0xf6, 0xd4, 0x85, 0x9d, 0xc4, 0xe7, 0xff, 0x7c, 0xf9, 0x76, 0xa1, 0xfb, 0xbb, 0xee, 0xd8, 0x43, + 0xb7, 0xfb, 0x5d, 0xb7, 0xfb, 0xb5, 0xdb, 0xf3, 0xae, 0xdb, 0xba, 0x76, 0xb6, 0x86, 0x70, 0xbd, + 0xb2, 0xba, 0xb9, 0xfa, 0x2d, 0xc2, 0x1c, 0x6c, 0xf8, 0x8d, 0x63, 0x18, 0xa9, 0x37, 0xf9, 0x39, + 0x83, 0x83, 0x15, 0xbd, 0xa2, 0x11, 0x4d, 0x9f, 0x96, 0x51, 0xbb, 0x18, 0x3f, 0x60, 0x8e, 0xe1, + 0xa3, 0x37, 0x10, 0xd9, 0xd7, 0x46, 0x92, 0x0a, 0x59, 0x33, 0x89, 0xd4, 0xbb, 0x75, 0x37, 0x33, + 0x6c, 0x2e, 0xa0, 0xc0, 0x72, 0x00, 0x6d, 0x90, 0x35, 0x7b, 0x03, 0x31, 0x10, 0x44, 0xb7, 0x0b, + 0x33, 0x33, 0x53, 0x67, 0x76, 0x1d, 0x66, 0xa6, 0x72, 0x4c, 0xeb, 0x44, 0x9f, 0xa5, 0xd3, 0x9d, + 0x20, 0xf0, 0xeb, 0xc3, 0x60, 0x38, 0x71, 0xf5, 0xe6, 0x69, 0xe8, 0x44, 0xc8, 0xd7, 0x7d, 0x8d, + 0x9b, 0xf4, 0x75, 0x1a, 0xf6, 0x25, 0x99, 0xca, 0xe7, 0x60, 0xb4, 0x56, 0xb0, 0x6c, 0x9e, 0xd8, + 0xd0, 0x2c, 0x74, 0xe9, 0xfb, 0x09, 0x15, 0x39, 0x47, 0x43, 0x8e, 0x8d, 0x12, 0x41, 0x51, 0xa2, + 0xfc, 0x58, 0x74, 0xd4, 0x8e, 0x43, 0x15, 0x7a, 0x69, 0x19, 0x17, 0xd2, 0x5b, 0x18, 0x0e, 0xb9, + 0xe8, 0xd8, 0x50, 0xfb, 0x16, 0x17, 0x15, 0x92, 0xf7, 0x28, 0x6b, 0x15, 0x16, 0x03, 0xa1, 0x03, + 0x98, 0x62, 0x45, 0x68, 0x94, 0x24, 0xd3, 0x12, 0x42, 0x4b, 0xf3, 0x55, 0x2f, 0x25, 0xcd, 0x7b, + 0x5b, 0xa2, 0xf9, 0x67, 0x51, 0x15, 0xd4, 0x5f, 0x13, 0xd6, 0x42, 0xe9, 0x0a, 0x2b, 0xea, 0xff, + 0x03, 0x8d, 0xa3, 0x64, 0x44, 0xe5, 0x81, 0xc1, 0x36, 0x0c, 0xf4, 0x33, 0x4d, 0x34, 0xea, 0x0c, + 0xd4, 0xa9, 0x4e, 0x35, 0xa9, 0xb6, 0xef, 0x70, 0xb9, 0x16, 0x2b, 0x30, 0xf3, 0x99, 0xda, 0x53, + 0x97, 0xd7, 0xf3, 0x2f, 0xbe, 0x7b, 0x73, 0x80, 0x5d, 0x29, 0x1c, 0x7f, 0xb5, 0x91, 0x4b, 0xc5, + 0x02, 0x36, 0x3e, 0x01, 0x13, 0x75, 0x80, 0x89, 0x3f, 0xc0, 0x42, 0x93, 0xc5, 0x64, 0x9d, 0xe1, + 0x72, 0xb3, 0xa1, 0xd4, 0xa6, 0x88, 0xed, 0x5c, 0x5c, 0x06, 0xe2, 0x0b, 0x41, 0xfd, 0x97, 0x41, + 0xed, 0x27, 0x9f, 0x84, 0xbb, 0x14, 0x92, 0x1f, 0x33, 0x17, 0xef, 0xfa, 0xd9, 0xf6, 0xea, 0xa7, + 0xf0, 0xea, 0x57, 0xcf, 0xa3, 0x75, 0xd8, 0x77, 0xad, 0x8f, 0x35, 0x5b, 0x04, 0x61, 0x0c, 0x84, + 0x83, 0x53, 0xe1, 0x52, 0xe1, 0x52, 0xe3, 0x50, 0xe3, 0xda, 0x60, 0xed, 0x3c, 0x2b, 0x39, 0x36, + 0x6b, 0xd9, 0x4f, 0xce, 0x7e, 0x3d, 0xee, 0x50, 0x5e, 0x19, 0x99, 0x99, 0x77, 0xe8, 0x52, 0xf2, + 0xf6, 0x89, 0xe2, 0xf6, 0x52, 0x6d, 0x80, 0xfb, 0x0d, 0xac, 0x63, 0x1d, 0x3b, 0xb0, 0x5c, 0x71, + 0xe0, 0xd9, 0xd2, 0x2c, 0x34, 0x74, 0x6c, 0x1b, 0x6c, 0x51, 0x18, 0xef, 0x41, 0xd3, 0xaa, 0x05, + 0xde, 0x36, 0xb4, 0xf0, 0xfb, 0x38, 0xf5, 0x2c, 0x29, 0xa4, 0x0c, 0x72, 0x44, 0x49, 0xe0, 0x89, + 0xae, 0xb6, 0x6f, 0x7c, 0x10, 0x65, 0xb8, 0x2b, 0xc5, 0xe3, 0x60, 0xe7, 0x12, 0x91, 0x16, 0x55, + 0x50, 0x8a, 0x96, 0xb0, 0x41, 0x2c, 0x8a, 0x30, 0x67, 0x5a, 0xa8, 0xbb, 0x90, 0x46, 0x33, 0x71, + 0x0e, 0x85, 0xc6, 0x5c, 0xd0, 0xe8, 0x67, 0xfa, 0xd8, 0xb7, 0xb0, 0x2e, 0x76, 0x09, 0x2a, 0x8d, + 0x32, 0xf6, 0x5f, 0x78, 0x13, 0xc8, 0x5f, 0xb8, 0xd5, 0x07, 0x28, 0x15, 0xdc, 0x7a, 0xde, 0xbd, + 0xa4, 0x3b, 0x2e, 0x9d, 0x5a, 0x47, 0xbd, 0x41, 0xdc, 0xe0, 0xd2, 0xd7, 0x2d, 0x5b, 0xb8, 0xd6, + 0xfa, 0xc6, 0xce, 0xc6, 0xbf, 0x04, 0xab, 0x2f, 0x04, 0x53, 0xdf, 0x08, 0xa6, 0x3e, 0x03, 0xbe, + 0xd5, 0xfd, 0xdd, 0x6e, 0x62, 0xaf, 0x83, 0x66, 0xbd, 0x52, 0xc8, 0x7f, 0x76, 0xa0, 0xc1, 0x0f, + 0x39, 0x88, 0x0a, 0x76, 0x49, 0xa9, 0xb9, 0x3e, 0xb4, 0xd1, 0x5c, 0xff, 0xbf, 0xcb, 0x5c, 0xad, + 0x9e, 0xe7, 0x34, 0xf7, 0x02, 0x31, 0xd7, 0x94, 0x9a, 0xd0, 0xf0, 0x37, 0x94, 0xe1, 0x4f, 0x94, + 0x27, 0x6d, 0x8f, 0xb5, 0x76, 0x43, 0x31, 0x0c, 0x40, 0x55, 0x66, 0x58, 0xca, 0xb4, 0x75, 0x0d, + 0xd3, 0x5e, 0xa6, 0xad, 0xb3, 0xce, 0x03, 0x25, 0xf1, 0x89, 0x6d, 0xbd, 0xca, 0x0e, 0xfd, 0x7d, + 0xc3, 0x6c, 0x46, 0xe9, 0x5e, 0x30, 0xe4, 0xea, 0x96, 0x35, 0xd7, 0xba, 0xd8, 0xce, 0x64, 0x51, + 0x28, 0xa1, 0xc0, 0x93, 0x60, 0x28, 0x2a, 0xae, 0x11, 0x92, 0x4b, 0x2c, 0xb7, 0xe1, 0x92, 0x43, + 0x47, 0xd2, 0x22, 0x41, 0xf3, 0xe7, 0x3d, 0x9c, 0x32, 0x1b, 0x1c, 0x1e, 0xc0, 0x35, 0xce, 0xdf, + 0x9d, 0x38, 0xb2, 0x8e, 0x05, 0x4e, 0x3c, 0x89, 0x51, 0x36, 0xd0, 0x18, 0xd5, 0x03, 0x0f, 0xf0, + 0xad, 0x2c, 0xb5, 0x1d, 0x19, 0x85, 0x86, 0xb2, 0x05, 0xed, 0xd2, 0x99, 0x49, 0x32, 0xb8, 0x41, + 0xcd, 0x12, 0x20, 0xfb, 0x00, 0x2d, 0x23, 0x75, 0x3c, 0x49, 0xff, 0xe3, 0x30, 0x92, 0x83, 0xb5, + 0xcf, 0x67, 0x74, 0x39, 0x8c, 0xd8, 0x24, 0x81, 0x55, 0x6c, 0x51, 0x82, 0x58, 0x31, 0x86, 0x9a, + 0xe0, 0xa2, 0xe9, 0x42, 0xb8, 0x68, 0xab, 0xaa, 0x82, 0x9d, 0x49, 0xb8, 0x93, 0x05, 0xfa, 0x0b, + 0x71, 0xd9, 0x5c, 0x16, 0x76, 0x67, 0xa8, 0x77, 0x46, 0x94, 0x3b, 0x0b, 0x94, 0x86, 0xc5, 0x11, + 0x86, 0x64, 0xa3, 0x3a, 0x4a, 0xd0, 0xc8, 0x17, 0x73, 0x99, 0x69, 0x5e, 0x38, 0xf4, 0xe9, 0x4a, + 0x71, 0x66, 0xbf, 0x9d, 0x08, 0xc7, 0xcd, 0xc8, 0xf7, 0xd6, 0x2b, 0x5e, 0xc3, 0x4f, 0x60, 0x19, + 0x5f, 0xb3, 0xf8, 0xab, 0xbd, 0x04, 0x53, 0x5b, 0xb0, 0x2a, 0x0c, 0x35, 0xa1, 0x8b, 0x84, 0xc8, + 0x62, 0xc4, 0xd6, 0x52, 0xe4, 0x57, 0xe9, 0x21, 0x3c, 0x56, 0x9b, 0x5a, 0xf7, 0xa6, 0x9e, 0x66, + 0x6f, 0xec, 0x7b, 0x22, 0x9c, 0x37, 0x94, 0x73, 0x68, 0x38, 0x26, 0xd3, 0xdb, 0x8c, 0xc3, 0x22, + 0x3c, 0xcc, 0x59, 0x23, 0x5c, 0x4c, 0x7d, 0x7b, 0xef, 0xe6, 0x44, 0x11, 0xfe, 0x01, 0x7d, 0x90, + 0x45, 0x5a, 0x04, 0x41, 0x0c, 0x46, 0x83, 0xbb, 0xbb, 0xed, 0x38, 0xc0, 0xc8, 0x09, 0xd8, 0xe2, + 0x70, 0x81, 0xb4, 0x65, 0x20, 0x2d, 0x95, 0x26, 0x55, 0xe8, 0xe9, 0xa9, 0x71, 0x9f, 0xee, 0x72, + 0x7b, 0xff, 0xf7, 0xc0, 0x70, 0x14, 0xe5, 0x84, 0x7f, 0xa4, 0x82, 0xb1, 0x18, 0x43, 0xb1, 0xc3, + 0x58, 0x8a, 0x32, 0x34, 0x2c, 0x06, 0x35, 0x4c, 0x58, 0xb0, 0x79, 0x00, 0xae, 0x1b, 0x9f, 0x79, + 0xee, 0xbb, 0x4f, 0x1b, 0xf9, 0xf6, 0x9b, 0x1b, 0x8c, 0x70, 0xa8, 0x54, 0x52, 0xe8, 0x48, 0x11, + 0xae, 0x22, 0xe5, 0xe4, 0x8d, 0x90, 0x6c, 0x69, 0xe4, 0x1b, 0xe1, 0x48, 0xa4, 0x40, 0x4b, 0xfa, + 0xd5, 0xdc, 0x3c, 0x74, 0xa4, 0x05, 0x9b, 0x30, 0xc7, 0xf8, 0x3d, 0x74, 0x08, 0xf0, 0xac, 0xf2, + 0xc0, 0xb1, 0xca, 0x24, 0x14, 0x9c, 0x79, 0x04, 0xac, 0x65, 0x6c, 0x2d, 0x16, 0x92, 0x50, 0x01, + 0x6b, 0x5d, 0x0c, 0xec, 0x61, 0x07, 0xa3, 0xb6, 0x56, 0xaf, 0x35, 0xdf, 0xe7, 0x30, 0xaf, 0xc2, + 0x7e, 0x2e, 0x1a, 0xa2, 0xb8, 0x10, 0x8d, 0x20, 0xfd, 0x38, 0xd2, 0x26, 0xcb, 0x92, 0xb1, 0xa2, + 0x16, 0xd6, 0xfb, 0x51, 0x60, 0xad, 0xb5, 0xe8, 0xfb, 0xa1, 0x44, 0x00, 0xaf, 0xd5, 0x47, 0x9a, + 0x9c, 0xe6, 0xb8, 0x99, 0x66, 0x79, 0x20, 0xcd, 0x72, 0x2f, 0xcd, 0x36, 0xce, 0xe4, 0xae, 0x0c, + 0x70, 0x97, 0x3b, 0xdc, 0xe5, 0x61, 0xee, 0xdc, 0xcd, 0x1d, 0xd6, 0xab, 0xd5, 0xda, 0x64, 0x0f, + 0x4d, 0xe3, 0x6d, 0x1b, 0x67, 0x4d, 0xdf, 0xb3, 0x9c, 0xf4, 0xd5, 0xc3, 0x59, 0xcb, 0x0d, 0xec, + 0x4b, 0xd4, 0xd1, 0x5f, 0x7c, 0x38, 0x37, 0x24, 0xe1, 0x1f, 0x62, 0x08, 0x72, 0xf4, 0x06, 0x10, + 0x3e, 0xdd, 0x58, 0x0a, 0xc4, 0x30, 0x54, 0x9b, 0x77, 0x5a, 0xba, 0x39, 0xc0, 0xd6, 0x93, 0x7d, + 0x1d, 0x3d, 0x07, 0xb1, 0xf8, 0x61, 0x5b, 0x20, 0x8b, 0x70, 0xfb, 0xcd, 0xa1, 0x83, 0x4a, 0xf1, + 0x27, 0x9c, 0x86, 0x61, 0x44, 0x61, 0xce, 0x58, 0x49, 0x8e, 0x24, 0xc8, 0xee, 0xa7, 0xc9, 0x7b, + 0x55, 0x68, 0x2a, 0x95, 0xca, 0x02, 0x8d, 0x92, 0xe4, 0x58, 0x6c, 0x42, 0xbf, 0xb1, 0x0a, 0x0f, + 0x73, 0x3f, 0xa0, 0xd0, 0x8e, 0xac, 0x92, 0x60, 0x62, 0xb1, 0xc8, 0x6a, 0xb1, 0x30, 0xd2, 0x59, + 0x49, 0x3e, 0xff, 0xbe, 0x81, 0x15, 0x56, 0x62, 0xb7, 0xc3, 0x34, 0xf5, 0x23, 0x9e, 0xc6, 0x65, + 0x3f, 0x98, 0xbf, 0x6f, 0x58, 0x1d, 0x4a, 0x5c, 0x47, 0x0a, 0xff, 0x9b, 0xfb, 0xd9, 0x4c, 0x03, + 0x7a, 0xce, 0x3b, 0x5b, 0x22, 0x17, 0x14, 0x1b, 0x22, 0xa3, 0x4b, 0x04, 0x2d, 0xe9, 0x86, 0xa4, + 0x90, 0x42, 0xbb, 0x3e, 0xa4, 0x04, 0xed, 0xa1, 0x3a, 0x68, 0xb6, 0xb1, 0x56, 0xcc, 0x1c, 0x28, + 0x43, 0xf3, 0xe7, 0xc6, 0x49, 0x0c, 0xaf, 0x84, 0x52, 0xc7, 0x69, 0xfc, 0x0c, 0x15, 0x6d, 0x1a, + 0x2e, 0x5b, 0x7c, 0x19, 0xaf, 0x44, 0xb8, 0x1e, 0x70, 0xe9, 0xfb, 0x01, 0x6b, 0x62, 0x45, 0xff, + 0x1f, 0xa7, 0xfd, 0xd0, 0x0f, 0x1f, 0x7f, 0x14, 0x48, 0x90, 0x4b, 0x7a, 0x5f, 0x25, 0x16, 0x2f, + 0x0b, 0xa1, 0x87, 0x0a, 0x17, 0x52, 0x5c, 0xc0, 0x3c, 0x8d, 0xdf, 0x79, 0xa2, 0x78, 0x65, 0x01, + 0xb4, 0x4f, 0x03, 0x8f, 0x05, 0x41, 0x0c, 0xee, 0xd9, 0xb6, 0x1d, 0x9e, 0x15, 0x27, 0x67, 0x9b, + 0x71, 0x33, 0x58, 0xcc, 0x5b, 0x74, 0xd3, 0x99, 0x87, 0x7f, 0x7f, 0xb6, 0xc2, 0x0b, 0xea, 0x7e, + 0x80, 0x8b, 0xf5, 0x43, 0x5c, 0x5e, 0x5d, 0x5a, 0x40, 0xeb, 0xa5, 0x96, 0x18, 0x63, 0x62, 0x81, + 0xe4, 0x04, 0xa6, 0x84, 0x5e, 0xc9, 0x79, 0x9c, 0x8a, 0xf8, 0x65, 0x67, 0xa1, 0xee, 0xa1, 0x4f, + 0xcc, 0xe3, 0x44, 0x85, 0xc2, 0x41, 0xe5, 0xad, 0xb1, 0xc7, 0x9e, 0x0d, 0xf1, 0x4a, 0x04, 0xa8, + 0x28, 0xc9, 0x44, 0x6a, 0x28, 0x45, 0x16, 0xda, 0x10, 0xca, 0xd8, 0x83, 0xd1, 0xbc, 0x95, 0xd0, + 0x1a, 0x19, 0x6b, 0x31, 0x21, 0xed, 0x25, 0xd0, 0xca, 0x5e, 0xe6, 0x09, 0xe7, 0x31, 0xf4, 0xfe, + 0xcd, 0xd4, 0x46, 0x94, 0x3c, 0x34, 0x5c, 0xf0, 0x18, 0x5a, 0x9f, 0x87, 0x00, 0x97, 0x0f, 0xd9, + 0xdb, 0x14, 0x19, 0x5e, 0x52, 0x2c, 0x18, 0xdf, 0xb0, 0x6a, 0xf7, 0x53, 0x6d, 0x94, 0x70, 0xc4, + 0x33, 0xdb, 0x97, 0x67, 0x27, 0x54, 0x7c, 0x12, 0xf3, 0xa2, 0xe3, 0x51, 0x42, 0xfb, 0x11, 0x5d, + 0x88, 0x99, 0x93, 0x85, 0xd5, 0x8b, 0xc5, 0x2f, 0x2b, 0x2c, 0x1a, 0xdf, 0x40, 0xf7, 0xd7, 0x87, + 0xa9, 0xe7, 0x39, 0xd8, 0x47, 0xa2, 0x92, 0x11, 0x84, 0x4b, 0xf3, 0x97, 0x3f, 0x5a, 0x1c, 0x7e, + 0x3c, 0x6f, 0xfe, 0x60, 0xb1, 0xf9, 0x4d, 0x56, 0x37, 0x4a, 0x36, 0x3a, 0xf0, 0x90, 0xed, 0xe2, + 0xd2, 0x22, 0xb4, 0x7c, 0xb0, 0xd4, 0xf7, 0xe6, 0x1e, 0x1a, 0x96, 0xfe, 0x09, 0x18, 0x2e, 0x9d, + 0x70, 0x8f, 0x98, 0xf7, 0x73, 0x37, 0x02, 0x0a, 0xa6, 0xa7, 0x67, 0xe6, 0xa5, 0x23, 0x82, 0x0c, + 0x00, 0x6d, 0x51, 0x03, 0x8f, 0xc4, 0x50, 0x10, 0x9e, 0xe8, 0x6c, 0x5b, 0xd1, 0x19, 0xab, 0x18, + 0xe7, 0xf8, 0x8c, 0x70, 0x52, 0xcc, 0x36, 0xaf, 0x9a, 0x3e, 0x2c, 0xfe, 0xfd, 0x9a, 0x4d, 0xc3, + 0xf1, 0x7c, 0x80, 0x2f, 0x63, 0x19, 0xc1, 0x31, 0xbe, 0xe6, 0x30, 0x64, 0xcf, 0x13, 0xb1, 0x87, + 0x8a, 0x12, 0xb2, 0x0c, 0x29, 0xb4, 0x95, 0x70, 0x3d, 0x42, 0xd2, 0x49, 0xcc, 0x35, 0x98, 0xed, + 0xd7, 0x61, 0x35, 0xd5, 0x99, 0xce, 0xe3, 0xf3, 0xd7, 0x7b, 0x3e, 0x87, 0x0e, 0x47, 0x89, 0x15, + 0xb7, 0xcf, 0x29, 0xcb, 0x15, 0x8c, 0x76, 0x48, 0x70, 0x5c, 0xae, 0x84, 0x21, 0x1c, 0x57, 0xb4, + 0x0d, 0xc7, 0x35, 0x51, 0x16, 0xb0, 0x1c, 0x08, 0xad, 0x31, 0x62, 0x97, 0x22, 0x58, 0x4e, 0xff, + 0x84, 0x65, 0x95, 0x1b, 0x26, 0x7b, 0xc8, 0xb6, 0x26, 0x55, 0x25, 0x85, 0x91, 0x34, 0x06, 0xd6, + 0x99, 0x23, 0xec, 0x16, 0xe0, 0x30, 0xd5, 0x5b, 0xd3, 0x14, 0x6b, 0x56, 0xb0, 0xd2, 0x5f, 0xde, + 0x4e, 0x0d, 0xac, 0xf4, 0x97, 0x33, 0x0e, 0x2f, 0xf7, 0x96, 0xc1, 0xdc, 0xdd, 0xde, 0xde, 0x52, + 0x26, 0x91, 0xcd, 0x36, 0x85, 0xc9, 0x11, 0xf0, 0x93, 0x03, 0x9c, 0x8b, 0x18, 0xb2, 0xb2, 0x90, + 0x8d, 0x85, 0x31, 0x23, 0xd5, 0x5b, 0xb5, 0xd8, 0x0a, 0xb1, 0x7b, 0x54, 0xc3, 0xd4, 0xc8, 0xe7, + 0xc9, 0xfe, 0xa7, 0x6f, 0x7c, 0x20, 0x2b, 0xc2, 0xaf, 0x3c, 0x7e, 0x55, 0x12, 0x45, 0x11, 0xe1, + 0xd7, 0xff, 0x5d, 0xae, 0x94, 0x2d, 0xe2, 0x25, 0x26, 0x1a, 0xce, 0x3a, 0xf2, 0x6d, 0x8f, 0x20, + 0xd8, 0x1e, 0x20, 0x38, 0x4e, 0x59, 0x72, 0x32, 0xce, 0x6f, 0x67, 0x04, 0x41, 0x13, 0xb5, 0x50, + 0xb5, 0x56, 0x05, 0x31, 0x10, 0x9d, 0x2d, 0x71, 0x77, 0xfd, 0x04, 0x1c, 0x6a, 0x68, 0x71, 0xef, + 0xee, 0x59, 0x99, 0x07, 0x39, 0x2f, 0x2e, 0x2c, 0x7f, 0x8f, 0xbb, 0x6b, 0x95, 0x8c, 0x5d, 0xa3, + 0x7d, 0xac, 0x72, 0xae, 0xb0, 0x37, 0x8f, 0xbd, 0x64, 0x3d, 0x2b, 0xc6, 0xde, 0xf1, 0xec, 0xdc, + 0xd2, 0x1c, 0xbc, 0x31, 0x0a, 0x81, 0xfd, 0x19, 0x7b, 0x98, 0xe2, 0xee, 0xa3, 0x5c, 0x8c, 0x94, + 0x15, 0x9c, 0xab, 0xfb, 0xb5, 0xd2, 0x28, 0x9b, 0x6b, 0x61, 0x34, 0x7c, 0x5e, 0x09, 0x83, 0x42, + 0x32, 0x4d, 0xc3, 0x06, 0xea, 0x68, 0x8a, 0x10, 0xa0, 0x4c, 0xc5, 0x8a, 0x3a, 0x3c, 0x5b, 0xce, + 0x23, 0x7b, 0x1a, 0x78, 0x81, 0xd5, 0xf9, 0xc8, 0x42, 0xc3, 0x2f, 0x66, 0xad, 0xcb, 0x4b, 0xff, + 0x09, 0x9f, 0xb1, 0x0e, 0x31, 0xf7, 0x58, 0x9b, 0x47, 0x38, 0xbf, 0xf1, 0xf2, 0x04, 0xb2, 0xf7, + 0x01, 0x72, 0x14, 0x85, 0x17, 0xd5, 0x09, 0x83, 0x83, 0xd5, 0xa6, 0xa6, 0xee, 0x95, 0xaf, 0xed, + 0x65, 0xa7, 0x42, 0x37, 0x37, 0x4d, 0xc5, 0xd8, 0x98, 0x7d, 0xd3, 0xc6, 0x44, 0x0a, 0xc5, 0x3b, + 0x36, 0x9e, 0xca, 0x6e, 0x9b, 0x7b, 0xfb, 0xba, 0x91, 0xa4, 0xa4, 0x89, 0xab, 0x25, 0x91, 0xcb, + 0x5b, 0xac, 0x89, 0x5a, 0x34, 0xc4, 0x73, 0xc4, 0xdd, 0xc7, 0x60, 0x9e, 0xab, 0xbb, 0x04, 0x95, + 0xd0, 0x41, 0x4b, 0xc4, 0x30, 0x10, 0x05, 0xe0, 0xb9, 0x97, 0x7a, 0x13, 0xbc, 0x14, 0xfc, 0x0d, + 0x8a, 0x8a, 0x3f, 0x42, 0x0f, 0xe2, 0xfd, 0x91, 0x6e, 0xdf, 0xda, 0xb0, 0x49, 0xa6, 0xce, 0x64, + 0x5d, 0x7f, 0xbe, 0x68, 0x0f, 0x6e, 0x21, 0x08, 0x9e, 0x43, 0xde, 0xf7, 0xde, 0xc8, 0xeb, 0x1c, + 0xcb, 0xe1, 0x59, 0x27, 0xe2, 0xe9, 0x06, 0xa6, 0x9a, 0xe1, 0xb4, 0x0f, 0x9a, 0xf4, 0x95, 0x96, + 0x63, 0x09, 0x09, 0xbb, 0x39, 0x54, 0xe9, 0x6e, 0xb1, 0xd3, 0xbc, 0x84, 0x12, 0xb5, 0xc0, 0xc2, + 0x14, 0x15, 0x63, 0xa2, 0x0c, 0x47, 0x1f, 0xa5, 0x3f, 0x44, 0x77, 0x64, 0x9d, 0x98, 0xa5, 0x37, + 0x2e, 0x0c, 0x95, 0x26, 0x97, 0x48, 0x6a, 0x01, 0x5a, 0x03, 0x8a, 0x82, 0x9f, 0x95, 0xf6, 0x9d, + 0xe6, 0x2c, 0xae, 0xe6, 0x72, 0x71, 0x66, 0x75, 0x77, 0xcd, 0xf0, 0x6b, 0x2c, 0x2e, 0xc3, 0xfe, + 0x98, 0x92, 0x0c, 0x4e, 0x8b, 0x21, 0xad, 0xde, 0x70, 0x8a, 0xfb, 0xb8, 0x55, 0x5f, 0x7e, 0xd5, + 0xd1, 0xe2, 0xf4, 0x46, 0xd0, 0x97, 0xa2, 0x27, 0xe9, 0xff, 0xda, 0x74, 0xff, 0xdf, 0x4d, 0x9b, + 0xd2, 0x0f, 0xed, 0xdf, 0xac, 0x33, 0xad, 0xb0, 0xae, 0xcd, 0xdb, 0x8d, 0xb7, 0x77, 0xd2, 0x71, + 0x8d, 0x44, 0x7e, 0xaf, 0x75, 0x63, 0x5c, 0x61, 0xfb, 0xd6, 0x3d, 0xb6, 0x4c, 0xfc, 0x20, 0x58, + 0x15, 0xb4, 0x19, 0x9c, 0xe7, 0x42, 0xbe, 0x00, 0x6c, 0x50, 0x35, 0x76, 0xc3, 0x60, 0x0c, 0x56, + 0xe6, 0x32, 0x33, 0xe3, 0x16, 0xbe, 0x40, 0x99, 0x96, 0x72, 0x3b, 0xe9, 0xc9, 0xb1, 0x5e, 0x6b, + 0x94, 0x7f, 0x08, 0x1c, 0x3f, 0xcc, 0x9e, 0xc4, 0xfa, 0x00, 0xbe, 0xa9, 0xc6, 0xe6, 0x9f, 0x34, + 0xe3, 0x7b, 0x11, 0x5f, 0xde, 0xf2, 0xf9, 0x42, 0x1e, 0x2b, 0x12, 0x25, 0x14, 0x7b, 0x12, 0xa3, + 0x26, 0xd7, 0x13, 0x74, 0x42, 0x86, 0xd3, 0xaa, 0x71, 0x60, 0x35, 0xf0, 0x8c, 0xc1, 0x48, 0x5c, + 0x8e, 0x60, 0x35, 0x21, 0x6d, 0x3d, 0xdb, 0xde, 0xe2, 0x46, 0x6b, 0xdd, 0x65, 0x8d, 0x9a, 0x13, + 0x26, 0xdb, 0x4a, 0xc4, 0x31, 0xac, 0x6b, 0xad, 0x24, 0x52, 0xd6, 0xc2, 0xa5, 0x48, 0x84, 0xed, + 0xc6, 0xd4, 0x64, 0xb5, 0x7f, 0x01, 0xbb, 0x13, 0x93, 0xb5, 0xe1, 0x0d, 0x1c, 0x4c, 0xcc, 0x32, + 0x0f, 0xbf, 0x85, 0x5c, 0xa1, 0x8c, 0x0e, 0xb9, 0x7f, 0x9c, 0xc6, 0x16, 0x61, 0x7e, 0x40, 0x05, + 0x32, 0xad, 0xde, 0x87, 0xa6, 0x4a, 0xd0, 0x2a, 0x5e, 0x0a, 0xa9, 0xe2, 0x0e, 0xda, 0xe2, 0x16, + 0x06, 0x5c, 0x16, 0x47, 0xb0, 0xa1, 0xde, 0xda, 0xa9, 0x47, 0xf9, 0x7c, 0x3e, 0x87, 0x83, 0x05, + 0xf8, 0xf6, 0xa4, 0xf7, 0xf2, 0x25, 0xdd, 0xae, 0xbd, 0xf6, 0xc7, 0xb9, 0x11, 0xbb, 0xe6, 0x06, + 0xc7, 0x6b, 0x18, 0x8a, 0x26, 0x14, 0x4b, 0x18, 0x0b, 0x72, 0xa3, 0xd5, 0x8b, 0x29, 0x44, 0xc3, + 0xb1, 0x11, 0x6d, 0x60, 0x7e, 0x04, 0x7c, 0xf6, 0xd6, 0x8b, 0x83, 0xd4, 0xff, 0x4b, 0x23, 0xaf, + 0xe1, 0xc7, 0x23, 0xc1, 0xeb, 0x62, 0xea, 0xe2, 0x0e, 0x26, 0x06, 0x36, 0x9b, 0x03, 0xb9, 0x46, + 0xa2, 0x3c, 0x33, 0x2d, 0x93, 0x81, 0x03, 0x49, 0x23, 0x00, 0x6d, 0x8f, 0x05, 0x72, 0x03, 0x31, + 0x0c, 0x45, 0x55, 0x0e, 0x94, 0x99, 0x7b, 0x86, 0xe2, 0x05, 0x3a, 0xd4, 0x81, 0x32, 0x0d, 0x6a, + 0x16, 0xb4, 0xad, 0x97, 0xe4, 0x48, 0x0e, 0xdc, 0x3e, 0xcc, 0xc9, 0x80, 0xf9, 0xff, 0xf7, 0x0c, + 0xbf, 0xc6, 0x63, 0x7c, 0xbc, 0x45, 0x21, 0x4b, 0x9e, 0x23, 0x81, 0x22, 0x73, 0x86, 0x4a, 0x52, + 0x23, 0x81, 0xd5, 0x07, 0x0c, 0x38, 0xb3, 0x5e, 0x6e, 0x38, 0x47, 0xf1, 0x42, 0xc3, 0xe8, 0xa7, + 0x84, 0x50, 0x48, 0x8c, 0x2a, 0x66, 0x1c, 0x52, 0x06, 0x85, 0x41, 0x12, 0x61, 0x25, 0x97, 0xe8, + 0xfe, 0x66, 0x5e, 0x04, 0xce, 0xaa, 0xea, 0x43, 0x79, 0x2c, 0x56, 0x1e, 0x02, 0xf7, 0xd0, 0x17, + 0x13, 0xfe, 0x11, 0x8a, 0xde, 0xdc, 0xde, 0xc0, 0xfa, 0x18, 0x7f, 0xe5, 0xe3, 0x76, 0x7e, 0x1b, + 0x5a, 0x85, 0xe3, 0x4e, 0xe5, 0x5c, 0x13, 0xd8, 0x1e, 0x54, 0x92, 0xda, 0x9c, 0xeb, 0xb0, 0xc3, + 0x7e, 0xaf, 0x11, 0xb3, 0x8a, 0x73, 0x13, 0x5f, 0xdc, 0xc3, 0xc9, 0xbb, 0x82, 0x52, 0xae, 0x2c, + 0x50, 0xf8, 0x79, 0x7a, 0x99, 0xcb, 0xbe, 0xea, 0xb0, 0xcf, 0xa3, 0x6a, 0x9a, 0xc2, 0xb9, 0x92, + 0x18, 0xaf, 0x3d, 0x77, 0x4c, 0xce, 0xeb, 0x26, 0x32, 0xb0, 0x3e, 0xe6, 0xb3, 0x6e, 0x3d, 0x71, + 0xc6, 0x75, 0xe2, 0xd4, 0x68, 0xf7, 0x84, 0x24, 0xb0, 0x3e, 0x74, 0xdc, 0x9f, 0x72, 0x3c, 0x98, + 0xf4, 0xd8, 0x18, 0x73, 0x3c, 0x9a, 0x72, 0x5c, 0xef, 0x3b, 0xae, 0x3b, 0x92, 0xcc, 0xe4, 0x5e, + 0x8a, 0xc1, 0xbf, 0xe7, 0xa0, 0x05, }; +#endif for (unsigned i = 0; i < sizeof(IDS) / sizeof(IDS[0]); i++) { if (IDS[i] != target_id) continue; unsigned group = 0; while (i >= FIRSTS[group + 1]) group++; - uint8_t raw[512]; - unsigned int produced = sizeof raw; +#if defined(OTA_RAK15002_SD_STORE) && defined(COMPANION_RADIO_FULL) + // Rare display lookup: larger chunks save flash without growing the loop stack. + uint8_t* raw = (uint8_t*)malloc(DECODE_CHUNK_BYTES); + if (!raw) return nullptr; + struct Scratch { uint8_t* bytes; ~Scratch() { free(bytes); } } scratch = {raw}; +#else + uint8_t raw[DECODE_CHUNK_BYTES]; +#endif + unsigned int produced = DECODE_CHUNK_BYTES; if (tinf_uncompress_exact(raw, &produced, NAMES + OFFSETS[group], OFFSETS[group + 1] - OFFSETS[group]) != TINF_OK || produced != RAW_LENGTHS[group]) return nullptr; @@ -563,6 +784,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xb62a1e75, "heltec_tracker_v2_kiss_modem" }, { 0x9e64845d, "heltec_tracker_v2_repeater" }, { 0x19232e8b, "heltec_tracker_v2_repeater_bridge_espnow" }, + { 0x10f41878, "heltec_tracker_v2_repeater_bridge_rs232" }, { 0xfaf164f4, "heltec_tracker_v2_repeater_observer_mqtt" }, { 0x38b948b4, "heltec_tracker_v2_room_server" }, { 0x462ddb9b, "heltec_tracker_v2_room_server_observer_mqtt" }, @@ -605,6 +827,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xb016979a, "heltec_v4_3_tft_companion_radio_usb_femoff" }, { 0xb49b6b55, "heltec_v4_3_tft_companion_radio_wifi_femoff" }, { 0x01d8f124, "heltec_v4_companion_radio_ble" }, + { 0x64140fd9, "heltec_v4_companion_radio_ble_femoff" }, { 0x86fb85f0, "heltec_v4_companion_radio_ble_femon" }, { 0xd33b0986, "heltec_v4_companion_radio_ble_ps_femon" }, { 0x1a4d0096, "heltec_v4_companion_radio_usb" }, @@ -848,6 +1071,9 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x49d11eca, "MKE_s3_room_server" }, { 0xd41f0638, "MKE_s3_sensor" }, { 0xe3105eb8, "MKE_s3_terminal_chat" }, + { 0xba76cc33, "morse_bench_rak4631_companion" }, + { 0x944263f8, "morse_bench_t096_companion" }, + { 0xc7a8c754, "morse_production_rak4631_companion" }, { 0xf8259880, "Nano_G2_Ultra_repeater" }, { 0x063f1f00, "nibble_screen_connect_companion_radio_ble_" }, { 0x70d71f92, "nibble_screen_connect_companion_radio_full_" }, @@ -908,8 +1134,12 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x41e33a97, "RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors" }, { 0x29a0da19, "RAK_4631_repeater_lora_ota_no_external_sensors" }, { 0x29bc38dc, "RAK_4631_repeater_rak15001_slot_c_lora_ota" }, + { 0xd5fc71d4, "RAK_4631_repeater_rak15002_sd_seeder_only" }, { 0x05f5ffae, "RAK_4631_repeater_unified_lora_ota" }, { 0xc62da371, "RAK_4631_repeater_w25q16_lora_ota" }, + { 0x96fc4c26, "RAK_4631_room_server_ethernet_lora_ota_no_external_sensors" }, + { 0x8a6dc63b, "RAK_4631_room_server_lora_ota_no_external_sensors" }, + { 0xb48950bb, "RAK_4631_sensor_lora_ota_no_external_sensors" }, { 0xf1d3c5a8, "RAK_4631_terminal_chat" }, { 0xc6d55752, "RAK_WisMesh_Tag_companion_radio_ble" }, { 0x60683191, "RAK_WisMesh_Tag_companion_radio_usb" }, @@ -951,10 +1181,12 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xd62ed92e, "Station_G3_ESP32_r2_repeater_observer_mqtt" }, { 0x7abaf0d5, "Station_G3_ESP32_r2_room_server" }, { 0x87edc08b, "Station_G3_ESP32_r2_room_server_observer_mqtt" }, + { 0xc50b0e48, "Station_G3_ESP32_r2_sensor" }, { 0x3c49caf8, "Station_G3_ESP32_repeater" }, { 0x58689ef9, "Station_G3_ESP32_repeater_observer_mqtt" }, { 0x91878dd8, "Station_G3_ESP32_room_server" }, { 0x72a93c45, "Station_G3_ESP32_room_server_observer_mqtt" }, + { 0x347e087a, "Station_G3_ESP32_sensor" }, { 0x4dbf035e, "t1000e_companion_radio_ble" }, { 0xff4102d6, "t1000e_companion_radio_usb" }, { 0x2f9a9185, "t1000e_kiss_modem" }, @@ -1044,8 +1276,21 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x7190df40, "ThinkNode_M9_kiss_modem" }, { 0x609d50b3, "ThinkNode_M9_repeater_" }, { 0x5718c7ea, "ThinkNode_M9_room_server_" }, + { 0x7bbf1081, "Waveshare_S3_LR1121_companion_radio_ble" }, + { 0xb151c2aa, "Waveshare_S3_LR1121_companion_radio_usb" }, + { 0x3b0174fd, "Waveshare_S3_LR1121_kiss_modem" }, + { 0xc1fd2578, "Waveshare_S3_LR1121_partition_expander_repeater_observer_mqtt" }, + { 0xeb440352, "Waveshare_S3_LR1121_partition_expander_room_server_observer_mqtt" }, + { 0xa297f85a, "Waveshare_S3_LR1121_repeater" }, + { 0xcbe54778, "Waveshare_S3_LR1121_repeater_observer_mqtt" }, + { 0x0343bdf0, "Waveshare_S3_LR1121_room_server" }, + { 0x3de413b1, "Waveshare_S3_LR1121_room_server_observer_mqtt" }, { 0x37443a5b, "WHY2025_badge_companion_radio_ble_" }, { 0x603384a7, "WHY2025_badge_repeater_" }, + { 0x76456c88, "Wio_Tracker_L2_companion_radio_ble" }, + { 0x28c4948b, "Wio_Tracker_L2_companion_radio_usb" }, + { 0xb34a83c5, "Wio_Tracker_L2_repeater" }, + { 0x63fe9591, "Wio_Tracker_L2_room_server" }, { 0xce3ef40f, "wio_wm1110_repeater" }, { 0x475a2a6a, "WioTrackerL1_companion_radio_ble" }, { 0xf4175d2c, "WioTrackerL1_companion_radio_usb" }, diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index affda7897..b52f2f644 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -1383,7 +1383,7 @@ uint8_t EnvironmentSensorManager::detectWisBlockStorageHolds() { || (w25_id != 0 && w25_id != 0xFFFFFFUL)) { holds |= mesh::gps::ExternalStorage; } -#elif defined(OTA_QSPI_SHARED_WISBLOCK_SPI) || (defined(OTA_SD_STORE) && !defined(OTA_RAK15002_SD_CACHE)) +#elif defined(OTA_QSPI_SHARED_WISBLOCK_SPI) || (defined(OTA_SD_STORE) && !defined(OTA_RAK15002_SD_CACHE) && !defined(OTA_RAK15002_SD_STORE)) holds |= mesh::gps::ExternalStorage; #endif // Exact RAK15002 uses connector pin 5 (unswitched 3V3), not 3V3_S. Its diff --git a/test/fixtures/ota_sd_cache/mocks/SdFat.h b/test/fixtures/ota_sd_cache/mocks/SdFat.h index 9c7892d3f..548b8ea71 100644 --- a/test/fixtures/ota_sd_cache/mocks/SdFat.h +++ b/test/fixtures/ota_sd_cache/mocks/SdFat.h @@ -138,3 +138,14 @@ public: uint32_t bytesPerCluster() const { return sd_test::medium.cluster_bytes; } SdCard* card() { return sd_test::medium.connected ? &_card : nullptr; } }; + +// The optional RAK15002 STORE profile intentionally restricts the card to +// FAT16/FAT32. Its production aliases select SdFat32/FatFile; map those +// names to distinct classes over the same in-memory filesystem boundary. +// These must remain classes, matching the real library's forward declarations. +class SdFat32 : public SdFs {}; +class FatFile : public FsFile { +public: + FatFile() = default; + FatFile(const FsFile& file) : FsFile(file) {} +}; diff --git a/test/fixtures/ota_sd_cache/test.cpp b/test/fixtures/ota_sd_cache/test.cpp index 034d90418..1d8048bdc 100644 --- a/test/fixtures/ota_sd_cache/test.cpp +++ b/test/fixtures/ota_sd_cache/test.cpp @@ -18,7 +18,7 @@ using namespace mesh::ota; // hardware boundary: no boot authorization, flash writes, or apply call exists // in this executable. The real archive implementation is compiled separately. namespace mesh { namespace ota { -OtaStoreSdNrf52::OtaStoreSdNrf52() : _sd(new SdFs), _file(new FsFile) {} +OtaStoreSdNrf52::OtaStoreSdNrf52() : _sd(new OtaSdFs), _file(new OtaSdFile) {} OtaStoreSdNrf52::~OtaStoreSdNrf52() { delete _file; delete _sd; } bool OtaStoreSdNrf52::mount() { if (_mounted) return true; @@ -26,6 +26,7 @@ bool OtaStoreSdNrf52::mount() { if (!_mounted) fail("SD mount failed"); return _mounted; } +bool OtaStoreSdNrf52::busAccessAllowed() const { return _mounted && sd_test::medium.connected; } void OtaStoreSdNrf52::fail(const char* text) { snprintf(_error, sizeof(_error), "%s", text); } bool OtaStoreSdNrf52::begin(uint32_t) { assert(false); return false; } bool OtaStoreSdNrf52::write(uint32_t, const uint8_t*, uint32_t) { assert(false); return false; } @@ -69,6 +70,13 @@ static void published(OtaCacheSdNrf52& cache) { assert(cache.describe(0, d) && !cache.describe(1, d)); assert(d.target_id == SIM_TARGET_ID && d.total_size == length && d.block_count == 2); assert(d.block_size_log2 == 11 && d.flags == (MFLAG_FULL | MFLAG_SIGNED)); + MotaManifest manifest; + assert(mota_parse_manifest(bytes + 8, MOTA_MFL, manifest)); + assert(!memcmp(d.mid, manifest.merkle_root, 4)); + assert(d.fw_version == manifest.fw_version && d.codec_id == manifest.codec_id); + assert(d.source_caps == 0 && d.leaves_off == 8 + MOTA_MFL); + assert(d.payload_off == d.leaves_off + manifest.block_count * 4); + assert(d.payload_size == manifest.payload_size); std::vector loaded(length); assert(cache.read(0, 0, loaded.data(), length)); assert(!memcmp(loaded.data(), bytes, length)); diff --git a/test/test_ethernet_terminal_shutdown.py b/test/test_ethernet_terminal_shutdown.py index cd4ff02e7..ccebb5e48 100644 --- a/test/test_ethernet_terminal_shutdown.py +++ b/test/test_ethernet_terminal_shutdown.py @@ -281,6 +281,7 @@ int main(){ # implementation is already exercised above. Reuse its method builder. methods = "\n".join(extract_braced(cpp,sig) for sig in ( "bool RAK13800EthernetInterface::begin()", + "bool RAK13800EthernetInterface::beginHardware()", "bool RAK13800EthernetInterface::terminalShutdown(", "bool RAK13800EthernetInterface::shutdownForPowerOff(", "void RAK13800EthernetInterface::disconnectClient()", @@ -301,6 +302,7 @@ int main(){ cls = extract_braced(header,"class RAK13800EthernetInterface")+";" signatures = ( "bool RAK13800EthernetInterface::begin()", + "bool RAK13800EthernetInterface::beginHardware()", "bool RAK13800EthernetInterface::terminalShutdown(", "bool RAK13800EthernetInterface::shutdownForPowerOff(", "void RAK13800EthernetInterface::disconnectClient()", diff --git a/test/test_firmware_ram.py b/test/test_firmware_ram.py index 615545ab7..d008d4ea7 100644 --- a/test/test_firmware_ram.py +++ b/test/test_firmware_ram.py @@ -751,6 +751,48 @@ int main() { with self.assertRaises(ValueError): ram.requirements(platform, definitions, "companion") + def test_optional_rak_sd_counts_lazy_volume_and_files_without_hiding_source_scratch(self): + flags = {"ENABLE_OTA": 1, "OTA_SD_STORE": 1} + base = ram.requirements("NRF52_PLATFORM", flags, "RAK_4631_repeater") + flags["OTA_RAK15002_SD_STORE"] = 1 + sd = ram.requirements("NRF52_PLATFORM", flags, "RAK_4631_repeater") + self.assertEqual(sd["required_heap_bytes"] - base["required_heap_bytes"], 2048) + self.assertEqual(sd["components"]["sd_fat_volume_and_files"], 2048) + self.assertEqual(sd["components"]["ota_generic_source_buffers"], + base["components"]["ota_generic_source_buffers"]) + + def test_optional_sd_room_ethernet_counts_exclusive_worker_but_companion_has_no_worker(self): + flags = {"ENABLE_OTA": 1, "OTA_SD_STORE": 1, "OTA_RAK15002_SD_STORE": 1, + "ETHERNET_ENABLED": 1, "RAK_4631": 1, "OTA_RAK_AUTO_STORE": 1, + "MESHCORE_OTA_DEVICE_DEFLATE": 1} + room = ram.requirements("NRF52_PLATFORM", flags, "RAK_4631_room_server") + parts = room["components"] + self.assertIn("ethernet_or_external_ota_workspaces", parts) + self.assertEqual(parts["ethernet_or_external_ota_workspaces"], 15440) + self.assertEqual(parts["sd_fat_volume_and_files"], 2048) + self.assertNotIn("ota_generic_source_buffers", parts) + large = ram.requirements("NRF52_PLATFORM", {**flags, "RAK4631_ETHERNET_TASK_STACK_WORDS": 4096}, + "RAK_4631_room_server") + self.assertEqual(large["components"]["ethernet_or_external_ota_workspaces"], 17216) + companion = ram.requirements("NRF52_PLATFORM", {**flags, "OTA_SEEDER_ONLY": 1}, + "RAK_4631_companion_radio_full") + self.assertNotIn("ethernet_or_external_ota_workspaces", companion["components"]) + self.assertIn("ota_generic_source_buffers", companion["components"]) + + def test_only_rak_sd_full_companion_counts_large_name_lookup(self): + flags = {"ENABLE_OTA": 1, "OTA_SD_STORE": 1, "OTA_RAK15002_SD_STORE": 1, + "OTA_TARGET_NAME_FRONT_CODED": 1, "COMPANION_RADIO_FULL": 1, "OTA_SEEDER_ONLY": 1} + full = ram.requirements("NRF52_PLATFORM", flags, "RAK_4631_companion_radio_full") + self.assertEqual(full["components"]["ota_target_name_lookup"], 4112) + for missing in ("OTA_RAK15002_SD_STORE", "OTA_TARGET_NAME_FRONT_CODED"): + other = ram.requirements("NRF52_PLATFORM", {key: value for key, value in flags.items() if key != missing}, + "RAK_4631_companion_radio_full") + self.assertNotIn("ota_target_name_lookup", other["components"]) + receiver = ram.requirements("NRF52_PLATFORM", {key: value for key, value in flags.items() + if key not in ("COMPANION_RADIO_FULL", "OTA_SEEDER_ONLY")}, + "RAK_4631_repeater_unified_lora_ota") + self.assertNotIn("ota_target_name_lookup", receiver["components"]) + def test_queue_sharing_applies_to_qualified_direct_full_builds_only(self): source = '#include "src/helpers/ota/OtaMemoryPolicy.h"\n#ifdef OTA_SHARED_COMPANION_QUEUE\nSHARING_ENABLED\n#endif\n' for flags, expected in ( diff --git a/test/test_nrf52_endf_hex.py b/test/test_nrf52_endf_hex.py index 18814a91f..63c304a4c 100644 --- a/test/test_nrf52_endf_hex.py +++ b/test/test_nrf52_endf_hex.py @@ -224,6 +224,47 @@ class Nrf52EndfHexTests(unittest.TestCase): stamp(action, path, env) self.assertEqual(path.read_bytes(), before) + def test_optional_rak_sd_preserves_adaptive_hybrid_and_companion_extrafs_layouts(self): + app_base = ml.NRF52_APP_BASE_S140_V6 + for companion in (False, True): + with self.subTest(companion=companion), tempfile.TemporaryDirectory() as directory: + env = BuildEnvironment(app_base) + env["CPPDEFINES"].extend(["RAK_4631", "OTA_SD_STORE", "OTA_RAK15002_SD_STORE"]) + if companion: + env.source_filter = "+" + env.maximum_size = ml.NRF52_EXTRAFS_START - app_base + env["CPPDEFINES"].extend(["OTA_SEEDER_ONLY", "EXTRAFS"]) + expected_flags = ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS + expected_ceiling = ml.NRF52_EXTRAFS_START + else: + env["CPPDEFINES"].extend(["OTA_RAK_AUTO_STORE", "OTA_QSPI_STORE", "OTA_FLASH_STORE", + "OTA_HYBRID_RAM_STORE", "OTA_INTERNAL_BOOTLOADER_UPDATE"]) + env.linker = str(ROOT / "boards/nrf52840_s140_v6_mota64.ld") + env["LDSCRIPT_PATH"] = env.linker + expected_flags = ml.NRF52_LAYOUT_FLAG_AUTO_STORE | ml.NRF52_LAYOUT_FLAG_HYBRID_RAM + expected_ceiling = ml.NRF52_APP_END + load_post_script(env) + path = Path(directory) / "firmware.hex" + original = IntelHex() + original.frombytes(bytes(range(256)), offset=app_base) + original.write_hex_file(str(path)) + action = env.post_actions["$BUILD_DIR/${PROGNAME}.hex"] + stamp(action, path, env) + layout = ml.parse_nrf52_layout(read_application(path)) + self.assertEqual(layout.flags, expected_flags) + self.assertEqual(layout.stage_ceiling, expected_ceiling) + before = path.read_bytes() + for missing in ("RAK_4631", "OTA_SD_STORE"): + env["CPPDEFINES"].remove(missing) + with self.assertRaisesRegex(RuntimeError, "optional RAK15002"): + stamp(action, path, env) + self.assertEqual(path.read_bytes(), before) + env["CPPDEFINES"].append(missing) + env["CPPDEFINES"].remove("OTA_SEEDER_ONLY" if companion else "OTA_RAK_AUTO_STORE") + with self.assertRaisesRegex(RuntimeError, "optional RAK15002"): + stamp(action, path, env) + self.assertEqual(path.read_bytes(), before) + def test_hex_exact_size_limit_and_smaller_profile_revalidated_after_stamp(self): app_base = ml.NRF52_APP_BASE_S140_V7 with tempfile.TemporaryDirectory() as directory: diff --git a/test/test_ota_sd_cache.py b/test/test_ota_sd_cache.py index d4e6cb297..8e5e07afd 100644 --- a/test/test_ota_sd_cache.py +++ b/test/test_ota_sd_cache.py @@ -14,6 +14,7 @@ FIXTURE = ROOT / "test/fixtures/ota_sd_cache" class OtaSdCacheTests(unittest.TestCase): + extra_defines = () @classmethod def setUpClass(cls): compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++") @@ -27,6 +28,7 @@ class OtaSdCacheTests(unittest.TestCase): compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", "-Wno-sign-compare", *SANITIZER_FLAGS, "-DNRF52_PLATFORM=1", "-DOTA_SD_STORE=1", "-DOTA_SD_CACHE_MAX=16", + *["-D" + define for define in cls.extra_defines], "-I", str(FIXTURE / "mocks"), "-I", str(ROOT / "src"), "-I", str(ROOT / "test/mocks"), str(FIXTURE / "test.cpp"), *[str(ROOT / "src/helpers/ota" / name) for name in source_names], @@ -59,5 +61,11 @@ class OtaSdCacheTests(unittest.TestCase): def test_full_index_refuses_seventeenth_without_losing_resumable_partial(self): self.run_case(10) +class RakOtaSdCacheTests(OtaSdCacheTests): + # Run all production capture/serve/reboot/error boundaries against the + # lossless bounded RAK descriptor index as well as the original index. + extra_defines = ("OTA_RAK15002_SD_STORE=1",) + + if __name__ == "__main__": unittest.main() diff --git a/test/test_ota_target_names.py b/test/test_ota_target_names.py index 0d6ffba47..4213d9841 100644 --- a/test/test_ota_target_names.py +++ b/test/test_ota_target_names.py @@ -91,10 +91,23 @@ class OtaTargetNameTest(unittest.TestCase): def test_cpp_standard_and_compact_match_all_names(self): ids = [target for target, _ in self.rows] + [0, 0xffffffff] expected = [name for _, name in self.rows] + ["?", "?"] - for flags in ([], ["-DOTA_TARGET_NAME_FRONT_CODED=1"]): + for flags in ([], ["-DOTA_TARGET_NAME_FRONT_CODED=1"], + ["-DOTA_TARGET_NAME_FRONT_CODED=1", "-DOTA_RAK15002_SD_STORE=1", "-DCOMPANION_RADIO_FULL=1"]): with self.subTest(flags=flags): self.assertEqual(self.compile_and_lookup(flags, ids), expected) + def test_rak_full_lookup_fails_closed_without_heap_and_keeps_every_name(self): + flags = ["-DOTA_TARGET_NAME_FRONT_CODED=1", "-DOTA_RAK15002_SD_STORE=1", "-DCOMPANION_RADIO_FULL=1"] + data, offsets, lengths, firsts = gen_targets.compressed_name_chunks( + self.rows, gen_targets.RAK_FULL_NAME_DECODE_CHUNK_SIZE) + ordinary = gen_targets.compressed_name_chunks(self.rows) + self.assertGreater(len(ordinary[0]) - len(data), 1500) + self.assertLessEqual(max(lengths), 4096) + self.assertEqual(firsts[-1], len(self.rows)) + self.assertEqual(offsets[-1], len(data)) + no_heap = '#include \n#define malloc(bytes) nullptr\n' + self.text + self.assertEqual(self.compile_and_lookup(flags, [self.rows[0][0], 0], no_heap), ["?", "?"]) + def test_table_disabled_preserves_optional_local_name(self): local_id, local_name = self.rows[0] ids = [local_id, self.rows[1][0], 0] @@ -128,8 +141,9 @@ class OtaTargetNameTest(unittest.TestCase): lambda match: match[1] + f"\n 0, {len(stream)}," + match[2], altered, flags=re.S) with self.subTest(stream=stream.hex()): - self.assertEqual(self.compile_and_lookup( - ["-DOTA_TARGET_NAME_FRONT_CODED=1"], [1], altered), ["?"]) + for flags in (["-DOTA_TARGET_NAME_FRONT_CODED=1"], + ["-DOTA_TARGET_NAME_FRONT_CODED=1", "-DOTA_RAK15002_SD_STORE=1", "-DCOMPANION_RADIO_FULL=1"]): + self.assertEqual(self.compile_and_lookup(flags, [1], altered), ["?"]) def test_generator_rejects_unencodable_names(self): for name in ("x\0y", "x" * 256): diff --git a/test/test_rak15002_sd_seeder.py b/test/test_rak15002_sd_seeder.py index 0bb7cb5f3..5f5a7cdac 100644 --- a/test/test_rak15002_sd_seeder.py +++ b/test/test_rak15002_sd_seeder.py @@ -104,6 +104,24 @@ class Rak15002SdSeederTests(unittest.TestCase): recipes = Recipes() self.assertEqual(recipes.config.get(ENV, "extends"), BASE) flags = recipes.value(ENV, "build_flags") + unflags = recipes.value(ENV, "build_unflags") + + # The ordinary reduced receiver is now adaptive internal/NOR/SD, but + # this historical exact profile must still compile as its distinct + # CACHE-only source. Check the effective compiler macro set rather + # than treating a parent recipe's library dependency as a child store. + def macros(text, directive): + return set(re.findall( + r"(?:^|\s)-" + directive + r"\s*([A-Za-z_][A-Za-z0-9_]*)", + text, + )) + + effective = macros(flags, "D") + effective -= macros(flags, "U") + # PlatformIO's build_unflags removes inherited compiler arguments; + # -D entries here deliberately fence an adaptive parent macro. + effective -= macros(unflags, "D") + effective -= macros(unflags, "U") for flag in ("OTA_SEEDER_ONLY=1", "OTA_SD_STORE=1", "OTA_RAK15002_SD_CACHE=1", "OTA_SD_CS_PIN=26", "OTA_SD_SCK_MHZ=4", "OTA_SD_CACHE_MAX=16", "OTA_MAX_SERVE=24", "OTA_HEAP_CONTEXT=1"): @@ -114,13 +132,20 @@ class Rak15002SdSeederTests(unittest.TestCase): self.assertIn(flag, flags) for flag in ("OTA_QSPI_STORE", "OTA_RAK_AUTO_STORE", "OTA_TOWER_AUTO_STORE", "OTA_SD_BOOTLOADER_UPDATE", "COMPANION_RADIO_FULL", "ETHERNET_ENABLED"): - self.assertNotRegex(flags, r"-D\s*" + flag + r"(?:=|\s|$)") + self.assertNotIn(flag, effective) + self.assertNotIn("OTA_RAK15002_SD_STORE", effective) + self.assertNotIn("OTA_FLASH_STORE", effective) + for flag in ("OTA_SEEDER_ONLY", "OTA_SD_STORE", "OTA_RAK15002_SD_CACHE", + "OTA_HEAP_CONTEXT"): + self.assertIn(flag, effective) libraries = recipes.value(ENV, "lib_deps") self.assertIn("SdFat-beta.git#a249cfa", libraries) self.assertIn("u-blox", libraries) self.assertIn("SSD1306", libraries) - self.assertNotIn("OTA_RAK15002_SD_CACHE", recipes.value(BASE, "build_flags")) - self.assertNotIn("SdFat", recipes.value(BASE, "lib_deps")) + base_flags = recipes.value(BASE, "build_flags") + self.assertIn("OTA_RAK15002_SD_STORE=1", base_flags) + self.assertNotIn("OTA_RAK15002_SD_CACHE", base_flags) + self.assertIn("SdFat", recipes.value(BASE, "lib_deps")) def test_real_bus_keeps_radio_pins_and_cs_safe(self): build(self, BUS_TEST, DEFINES, {"SPI.h": SPI_MOCK}) @@ -167,6 +192,7 @@ class EnvironmentSensorManager { public: uint8_t detectWisBlockStorageHolds(); } int main() { EnvironmentSensorManager sensors; assert(sensors.detectWisBlockStorageHolds() == EXPECT_HOLD); } '''.replace("@METHOD@", method) for defines in (["OTA_SD_STORE", "OTA_RAK15002_SD_CACHE", "EXPECT_HOLD=0"], + ["OTA_SD_STORE", "OTA_RAK15002_SD_STORE", "EXPECT_HOLD=0"], ["OTA_SD_STORE", "EXPECT_HOLD=mesh::gps::ExternalStorage"], ["OTA_QSPI_SHARED_WISBLOCK_SPI", "EXPECT_HOLD=mesh::gps::ExternalStorage"], ["EXPECT_HOLD=0"]): diff --git a/test/test_rak15002_shared_spi.py b/test/test_rak15002_shared_spi.py new file mode 100644 index 000000000..a56e754ae --- /dev/null +++ b/test/test_rak15002_shared_spi.py @@ -0,0 +1,458 @@ +"""Execute RAK15002 SD/NOR/Ethernet lifetime guards without hardware or PIO.""" +from pathlib import Path +import os +import shutil +import subprocess +import sys +import tempfile +import unittest + +from test_replay_reset_integration import extract_braced +from test_ethernet_terminal_shutdown import SYNC + +ROOT = Path(__file__).resolve().parents[1] +FLAGS = ["NRF52_PLATFORM", "RAK_4631", "OTA_SD_STORE", "OTA_RAK15002_SD_STORE", + "SPI_INTERFACES_COUNT=1", "PIN_SPI_MISO=29", "PIN_SPI_SCK=3", "PIN_SPI_MOSI=30", + "WB_IO6=10", "RAK4631_COMBINED_ETHERNET"] +SPI = r''' +#pragma once +#include +#include +#include +constexpr int INPUT=0,OUTPUT=1,INPUT_PULLUP=2,LOW=0,HIGH=1; +struct Peripheral { unsigned id; }; +inline Peripheral periph1{1},periph2{2},periph3{3}; +#define NRF_SPIM1 (&periph1) +#define NRF_SPIM2 (&periph2) +#define NRF_SPIM3 (&periph3) +inline bool inserted=false; +inline unsigned begin_count[4]={},end_count[4]={}; +inline std::vector touched; +inline void pinMode(int pin,int){touched.push_back(pin);} +inline void digitalWrite(int pin,int){touched.push_back(pin);} +inline int digitalRead(int pin){assert(pin==10);return inserted?LOW:HIGH;} +struct SPIClass { + Peripheral* peripheral; int miso,sck,mosi; + SPIClass(Peripheral* p,int i,int c,int o):peripheral(p),miso(i),sck(c),mosi(o){} + void begin(){++begin_count[peripheral->id];} + void end(){++end_count[peripheral->id];} +}; +inline SPIClass SPI(NRF_SPIM3,45,43,44),SPI1(NRF_SPIM2,9,10,11); +''' +PRELUDE = r''' +#include +#include +#include +#include +#include +using namespace mesh::ota; +''' + + +def run_native(test, source, files=None, defines=FLAGS, negative=False): + compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++") + if not compiler: + test.skipTest("host C++17 compiler required") + with tempfile.TemporaryDirectory(prefix="rak-shared-spi-") as folder: + work = Path(folder) + sources = {"main.cpp": source, "SPI.h": SPI, "Arduino.h": '#pragma once\n#include "SPI.h"\n'} + sources.update(files or {}) + for name, content in sources.items(): + (work / name).write_text(content) + sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"] + if sys.platform.startswith("linux") else []) + command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", + *sanitizer, *["-D" + item for item in defines], "-I" + str(work), + "-I" + str(ROOT / "src"), *[str(work / name) for name in sources if name.endswith(".cpp")], + "-o", str(work / "case")] + built = subprocess.run(command, text=True, capture_output=True, timeout=60) + test.assertEqual(built.returncode, 0, built.stdout + built.stderr) + ran = subprocess.run([str(work / "case")], text=True, capture_output=True, timeout=15) + if negative: + test.assertNotEqual(ran.returncode, 0) + else: + test.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) + + +class Rak15002SharedSpiTests(unittest.TestCase): + def test_shared_nor_ethernet_compile_gate_accepts_only_admitted_runtime_profiles(self): + header = (ROOT / "src/helpers/ota/OtaStoreQspiNrf52.h").read_text() + guards = header.split("#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(RAK_3401)", 1)[0] + base = {"NRF52_PLATFORM", "RAK_4631", "OTA_RAK_AUTO_STORE", + "OTA_QSPI_SHARED_WISBLOCK_SPI", "ETHERNET_ENABLED"} + compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++") + if not compiler: + self.skipTest("host C++17 compiler required") + for extra, missing, admitted in ( + ({"RAK4631_COMBINED_ETHERNET"}, set(), True), + ({"OTA_RAK15002_SD_STORE", "OTA_SD_STORE"}, set(), True), + (set(), set(), False), + ({"OTA_RAK15002_SD_STORE"}, {"RAK_4631"}, False), + ({"OTA_RAK15002_SD_STORE"}, {"OTA_RAK_AUTO_STORE"}, False), + ({"OTA_RAK15002_SD_STORE"}, {"NRF52_PLATFORM"}, False)): + with self.subTest(extra=extra, missing=missing): + built = subprocess.run([compiler, "-x", "c++", "-fsyntax-only", + *["-D" + value for value in sorted((base | extra) - missing)], "-"], + input=guards + "\nint main() {}\n", capture_output=True, text=True, timeout=15) + self.assertEqual(built.returncode == 0, admitted, built.stderr) + + def test_owner_shared_across_translation_units_no_legacy_setter_steal(self): + app = PRELUDE + r''' +void* secondOwner(); +int main(){ + assert(secondOwner()==&rak4631_shared_spi_owner()); + assert(!rak4631_try_acquire_spi(RakSharedSpiOwner::None)); + for(auto device:{RakSharedSpiOwner::Sd,RakSharedSpiOwner::Nor}){ + assert(rak4631_try_acquire_spi(device)); + assert(rak4631_try_acquire_spi(device)); + rak4631_set_ethernet_spi_owner(true);assert(!rak4631_ethernet_owns_spi()); + rak4631_set_ethernet_spi_owner(false);assert(rak4631_spi_owner()==device); + assert(!rak4631_try_acquire_spi(RakSharedSpiOwner::Ethernet)); + rak4631_release_spi(RakSharedSpiOwner::Ethernet);assert(rak4631_spi_owner()==device); + rak4631_release_spi(device);assert(rak4631_spi_owner()==RakSharedSpiOwner::None); + } + rak4631_set_ethernet_spi_owner(true);assert(rak4631_ethernet_owns_spi()); + assert(!rak4631_try_acquire_spi(RakSharedSpiOwner::Sd)); + rak4631_set_ethernet_spi_owner(false);assert(!rak4631_ethernet_owns_spi()); +} +''' + other = PRELUDE + "void* secondOwner(){return &rak4631_shared_spi_owner();}" + run_native(self, app, {"second.cpp": other}) + header = (ROOT / "src/helpers/nrf52/Rak4631SharedSpi.h").read_text() + broken = header.replace("inline std::atomic& rak4631_shared_spi_owner()", + "static std::atomic& rak4631_shared_spi_owner()") + self.assertNotEqual(header, broken) + local_app = app.replace("", '"Owner.h"') + local_other = other.replace("", '"Owner.h"') + run_native(self, local_app, {"second.cpp": local_other, "Owner.h": broken}, negative=True) + + def test_sd_bus_and_active_low_card_detect_never_reconfigure_radio(self): + run_native(self, PRELUDE + r''' +int main(){ + assert(&ota_sd_spi_bus()!=&SPI&&&ota_sd_spi_bus()!=&SPI1); + assert(ota_sd_spi_bus().peripheral==NRF_SPIM2); + assert(ota_sd_spi_bus().miso==29&&ota_sd_spi_bus().sck==3&&ota_sd_spi_bus().mosi==30); + assert(SPI.peripheral==NRF_SPIM3&&SPI.miso==45&&SPI.sck==43&&SPI.mosi==44); + assert(!ota_rak15002_sd_card_present());inserted=true;assert(ota_rak15002_sd_card_present()); + for(auto pin:touched)assert(pin==10); + assert(!begin_count[1]&&!begin_count[2]&&!begin_count[3]); +} +''') + + def test_rak_store_uses_fat32_aliases_without_changing_legacy_sd_types(self): + # The new optional RAK15002 STORE profile intentionally excludes + # exFAT. Existing MeshTower and CACHE-only profiles retain the wider + # SdFs/FsFile type pair, so both selections are compiled explicitly. + source = (ROOT / "src/helpers/ota/OtaStoreSdNrf52.cpp").read_text() + bound_start = source.index("static_assert(sizeof(") + bound = source[bound_start:source.index(");", bound_start) + 2] + run_native(self, r''' +#include +class SdFat32 {}; +class FatFile {}; +#include +static_assert(std::is_same::value, "RAK store must be FAT16/FAT32 only"); +static_assert(std::is_same::value, "RAK store must use FatFile"); +@BOUND@ +int main() {} +'''.replace("@BOUND@", bound), {"SdFat.h": '#error "SD public headers must not leak SdFat File into LittleFS callers"\n'}, + defines=["NRF52_PLATFORM", "OTA_SD_STORE", "OTA_RAK15002_SD_STORE"]) + run_native(self, r''' +#include +class SdFs {}; +class FsFile {}; +#include +static_assert(std::is_same::value, "legacy SD must retain SdFs"); +static_assert(std::is_same::value, "legacy SD must retain FsFile"); +int main() {} +''', defines=["NRF52_PLATFORM", "OTA_SD_STORE"]) + + def test_user_gpio_reserves_exact_sd_bus_and_detect_only_for_new_profile(self): + run_native(self, r''' +#include +#include +int main(){ + for(uint8_t pin=0;pin<48;++pin){ + const bool claimed=pin==3||pin==10||pin==26||pin==29||pin==30; + assert(UserGpioPinPolicy::isFirmwareReserved(pin)==claimed); + } +} +''') + run_native(self, r''' +#include +#include +int main(){for(uint8_t pin=0;pin<48;++pin)assert(!UserGpioPinPolicy::isFirmwareReserved(pin));} +''', defines=[]) + + def test_real_sd_mount_retains_owner_failed_acquire_has_no_spi_and_pins_release(self): + source = (ROOT / "src/helpers/ota/OtaStoreSdNrf52.cpp").read_text() + signatures = ("OtaStoreSdNrf52::OtaStoreSdNrf52()", "OtaStoreSdNrf52::~OtaStoreSdNrf52()", + "bool OtaStoreSdNrf52::acquireBus()", "bool OtaStoreSdNrf52::busAccessAllowed() const", + "void OtaStoreSdNrf52::releaseBusClaim()", "bool OtaStoreSdNrf52::releaseBus()", + "void OtaStoreSdNrf52::fail(", "void OtaStoreSdNrf52::resetStoreState()", + "bool OtaStoreSdNrf52::beginCardOnly()", "bool OtaStoreSdNrf52::mount()", + "bool OtaStoreSdNrf52::write(", "bool OtaStoreSdNrf52::read(", + "uint32_t OtaStoreSdNrf52::capacity() const", "void OtaStoreSdNrf52::checkpoint()") + receiver = source[source.index("bool OtaStoreSdNrf52::locate_file()") :] + install = ("bool OtaStoreSdNrf52::write(", "bool OtaStoreSdNrf52::read(", + "void OtaStoreSdNrf52::checkpoint()") + methods = "\n".join(extract_braced(receiver if name in install else source, name) for name in signatures) + mock = r''' +inline unsigned fs_allocations=0,file_allocations=0,io=0; +inline bool mount_ok=true; +inline void allowed(){assert(rak4631_spi_owner()==RakSharedSpiOwner::Sd);++io;} +struct SdSpiConfig{SdSpiConfig(int,int,int,SPIClass*){}}; +constexpr int DEDICATED_SPI=0; +#define SD_SCK_MHZ(x) (x) +#define OTA_SD_SCK_MHZ 4 +constexpr uint32_t MOTA_SD_AUTH_SECTOR_SIZE=512; +struct Card{uint32_t sectorCount(){allowed();return 2048;}bool syncDevice(){allowed();return true;}}; +class SdFs{public: SdFs(){++fs_allocations;} bool begin(SdSpiConfig){allowed();ota_sd_spi_bus().begin();return mount_ok;} + bool cardBegin(SdSpiConfig c){return begin(c);}void end(){allowed();}Card* card(){allowed();static Card c;return &c;}}; +class FsFile{public:bool opened=false;FsFile(){++file_allocations;}explicit operator bool()const{return opened;} + void close(){allowed();opened=false;}bool seekSet(uint32_t){allowed();return true;} + size_t write(const uint8_t*,uint32_t n){allowed();return n;}int read(uint8_t*,uint32_t n){allowed();return n;} + bool sync(){allowed();return true;}}; +using SdFat32=SdFs;using FatFile=FsFile; +namespace mesh{namespace ota{using OtaSdFs=::SdFat32;using OtaSdFile=::FatFile;}} +static void clear_sd_auth_record(){} +''' + header = (ROOT / "src/helpers/ota/OtaStoreSdNrf52.h").read_text() + cls = extract_braced(header, "class OtaStoreSdNrf52 : public OtaStore").replace("private:", "public:") + ";" + stubs = r''' +bool OtaStoreSdNrf52::begin(uint32_t){return false;} +bool OtaStoreSdNrf52::discard(){return false;}void OtaStoreSdNrf52::clear(){} +bool OtaStoreSdNrf52::set_meta_size(uint32_t){return false;} +bool OtaStoreSdNrf52::finalize(){return false;}bool OtaStoreSdNrf52::reopen(){return false;} +bool OtaStoreSdNrf52::plan_layout(bool,uint32_t,uint32_t,uint32_t,bool){return false;} +''' + runner = r''' +int main(){ + OtaStoreSdNrf52 store;assert(!fs_allocations&&!file_allocations);inserted=true; + for(auto peer:{RakSharedSpiOwner::Ethernet,RakSharedSpiOwner::Nor}){ + assert(rak4631_try_acquire_spi(peer));touched.clear();const unsigned before=io; + assert(!store.probeMedia());assert(io==before&&!fs_allocations&&!begin_count[2]); + for(auto pin:touched)assert(pin==10); + rak4631_release_spi(peer); + } + inserted=false;assert(!store.probeMedia()&&!fs_allocations); + inserted=true;mount_ok=false;assert(!store.probeMedia()); + assert(rak4631_spi_owner()==RakSharedSpiOwner::None&&begin_count[2]==1&&end_count[2]==1); + mount_ok=true;assert(store.probeMedia());assert(rak4631_spi_owner()==RakSharedSpiOwner::Sd); + assert(fs_allocations==1&&file_allocations==1&&!begin_count[3]&&!end_count[3]); + store._file->opened=true;store._total=20;uint8_t bytes[20]={}; + assert(store.read(0,bytes,20)&&store.write(0,bytes,20)); + assert(!store.releaseBus()&&rak4631_spi_owner()==RakSharedSpiOwner::Sd); + const unsigned before=io;inserted=false;assert(!store.read(0,bytes,20)&&!store.write(0,bytes,20)); + store.checkpoint();assert(!store.capacity()&&io==before); + store._total=0;assert(store.releaseBus());assert(rak4631_spi_owner()==RakSharedSpiOwner::None); + assert(!store._mounted&&!store._file->opened&&end_count[2]==2); + assert(rak4631_try_acquire_spi(RakSharedSpiOwner::Ethernet)); + const unsigned ended=end_count[2];assert(store.releaseBus()&&end_count[2]==ended); + assert(rak4631_ethernet_owns_spi());rak4631_set_ethernet_spi_owner(false); +} +''' + code = PRELUDE + '#include \n' + mock + code += "namespace mesh{class MainBoard;namespace ota{" + cls + methods + stubs + "}}\n" + runner + run_native(self, code) + + def test_actual_nor_discovery_skips_card_eth_and_sd_owners_without_touching_pads(self): + source = (ROOT / "src/helpers/ota/OtaStoreQspiNrf52.cpp").read_text() + methods = "\n".join(extract_braced(source, name) for name in ( + "uint8_t OtaStoreQspiNrf52::autoDetect()", + "bool OtaStoreQspiNrf52::headerW25Detected()", + "void OtaStoreQspiNrf52::autoProbeIds(")) + harness = PRELUDE + r''' +namespace mesh { namespace ota { +static uint8_t auto_cs_pin=0xff,auto_detection=0xff; +static uint32_t auto_jedec_id=0,auto_rak_probe_id=0,auto_w25_probe_id=0; +static bool auto_header_w25=false; +constexpr uint8_t HEADER_SCK_PIN=16,HEADER_IO0_PIN=17,HEADER_IO1_PIN=15; +static unsigned gpio=0,probes=0; +uint8_t arduino_to_physical(uint32_t pin){return pin;} +void nrf_gpio_pin_set(uint32_t){++gpio;} +void nrf_gpio_cfg_output(uint32_t){++gpio;} +void nrf_gpio_cfg_default(uint32_t){++gpio;} +uint32_t probe_nor(uint8_t cs){ + assert(!inserted&&rak4631_spi_owner()==RakSharedSpiOwner::Nor);++probes; + return cs==31?0xef4015:0; +} +class OtaStoreQspiNrf52 {public: + static uint8_t autoDetect();static bool headerW25Detected(); + static void autoProbeIds(uint32_t&,uint32_t&); +}; +@METHODS@ +} } +int main(){ + inserted=true;assert(!OtaStoreQspiNrf52::autoDetect()&&!gpio&&!probes); + inserted=false; + for(auto other:{RakSharedSpiOwner::Ethernet,RakSharedSpiOwner::Sd}){ + assert(rak4631_try_acquire_spi(other)); + assert(!OtaStoreQspiNrf52::autoDetect()&&!gpio&&!probes&&auto_detection==0xff); + rak4631_release_spi(other); + } + assert(OtaStoreQspiNrf52::autoDetect()==2&&probes==2&&auto_cs_pin==31&&auto_jedec_id==0xef4015); + assert(rak4631_spi_owner()==RakSharedSpiOwner::None); + const auto done=gpio;inserted=true; + assert(!OtaStoreQspiNrf52::autoDetect()&&gpio==done&&probes==2); + uint32_t rak=0,w25=0;OtaStoreQspiNrf52::autoProbeIds(rak,w25); + assert(rak==0&&w25==0xef4015&&gpio==done&&probes==2); +} +''' + run_native(self, harness.replace("@METHODS@", methods), + defines=FLAGS + ["OTA_RAK_AUTO_STORE"]) + + def test_real_archive_handles_recheck_owner_even_when_initialized(self): + source = (ROOT / "src/helpers/ota/OtaCacheSdNrf52.cpp").read_text() + methods = "\n".join(extract_braced(source, name) for name in ( + "bool OtaCacheSdNrf52::ready()", "bool OtaCacheSdNrf52::initialize()", + "bool OtaCacheSdNrf52::write(uint32_t", "bool OtaCacheSdNrf52::read(uint32_t", + "uint32_t OtaCacheSdNrf52::capacity() const", "void OtaCacheSdNrf52::checkpoint()")) + harness = PRELUDE + r''' +namespace mesh { namespace ota { +static unsigned calls=0; +static void access(){assert(inserted&&rak4631_spi_owner()==RakSharedSpiOwner::Sd);++calls;} +struct FsFile{explicit operator bool()const{return true;} + bool seekSet(uint32_t){access();return true;}size_t write(const uint8_t*,uint32_t n){access();return n;} + int read(uint8_t*,uint32_t n){access();return n;}bool sync(){access();return true;}}; +struct Card{uint32_t sectorCount(){access();return 2048;}bool syncDevice(){access();return true;}}; +struct SdFs{Card* card(){access();static Card c;return &c;}bool exists(const char*){access();return false;}}; +struct OtaStoreSdNrf52{bool _mounted=true;SdFs fs;SdFs* _sd=&fs; + bool busAccessAllowed()const{return inserted&&rak4631_spi_owner()==RakSharedSpiOwner::Sd;} + bool mount(){return busAccessAllowed();}const char* last_error()const{return "busy";}}; +class OtaCacheSdNrf52{public: + OtaStoreSdNrf52* _owner;FsFile* _file;uint32_t _total=20;bool _initialized=true,_enabled=true;bool failed=false;char _error[80]={}; + static constexpr const char* DISABLED_PATH="/mota/cache.off"; + bool ready();bool initialize();bool write(uint32_t,const uint8_t*,uint32_t); + bool read(uint32_t,uint8_t*,uint32_t)const;uint32_t capacity()const;void checkpoint(); + void fail(const char*){failed=true;}bool ensureDir(){access();return true;}bool rescan(){access();return true;} +}; +@METHODS@ +} } +int main(){ + OtaStoreSdNrf52 store;FsFile file;OtaCacheSdNrf52 cache;cache._owner=&store;cache._file=&file; + inserted=true;uint8_t bytes[20]={}; + assert(rak4631_try_acquire_spi(RakSharedSpiOwner::Sd)); + assert(cache.initialize()&&cache.write(0,bytes,20)&&cache.read(0,bytes,20)&&cache.capacity()); + cache.checkpoint();rak4631_release_spi(RakSharedSpiOwner::Sd); + const auto before=calls; + for(auto peer:{RakSharedSpiOwner::Ethernet,RakSharedSpiOwner::Nor}){ + assert(rak4631_try_acquire_spi(peer)); + assert(!cache.initialize()&&!cache.write(0,bytes,20)&&!cache.read(0,bytes,20)&&!cache.capacity()); + cache.checkpoint();assert(calls==before);rak4631_release_spi(peer); + } + assert(rak4631_try_acquire_spi(RakSharedSpiOwner::Sd));inserted=false; + assert(!cache.initialize()&&!cache.read(0,bytes,20)&&!cache.capacity());cache.checkpoint();assert(calls==before); +} +''' + for seeder in (False, True): + with self.subTest(source_only=seeder): + run_native(self, harness.replace("@METHODS@", methods), + defines=FLAGS + (["OTA_SEEDER_ONLY"] if seeder else [])) + + def test_source_only_volume_cannot_stage_write_resume_or_approve_installation(self): + source = (ROOT / "src/helpers/ota/OtaStoreSdNrf52.cpp").read_text() + # Preserve the actual preprocessor seam, not extracted method bodies: + # capacity must stay available to BOTH vtables, not hidden in #else. + gate = source.index("#if defined(OTA_SEEDER_ONLY)\n// The source-only context") + start = source.rfind("uint32_t OtaStoreSdNrf52::capacity() const", 0, gate) + self.assertGreaterEqual(start, 0, "volume capacity must be outside the receiver-only branch") + end = source.index("#endif // OTA_SEEDER_ONLY", gate) + len("#endif // OTA_SEEDER_ONLY") + methods = source[start:end] + header = (ROOT / "src/helpers/ota/OtaStoreSdNrf52.h").read_text() + cls = extract_braced(header, "class OtaStoreSdNrf52 : public OtaStore").replace("private:", "public:") + ";" + code = r''' +#include +#include +#include +static unsigned card_queries=0; +struct Card{uint32_t sectorCount(){++card_queries;return 2048;}}; +class SdFs{public:Card card_value;Card* card(){return &card_value;}}; +class FsFile; +using SdFat32=SdFs;using FatFile=FsFile; +namespace mesh{namespace ota{using OtaSdFs=::SdFat32;using OtaSdFile=::FatFile;}} +namespace mesh {class MainBoard;namespace ota { +static bool ready=false; +constexpr uint32_t MOTA_SD_AUTH_SECTOR_SIZE=512; +@CLASS@ +OtaStoreSdNrf52::OtaStoreSdNrf52()=default; +OtaStoreSdNrf52::~OtaStoreSdNrf52()=default; +bool OtaStoreSdNrf52::busAccessAllowed()const{return ready;} +void OtaStoreSdNrf52::fail(const char* message){strncpy(_error,message,sizeof(_error)-1);} +@METHODS@ +} } +int main(){ + mesh::ota::OtaStoreSdNrf52 volume;uint8_t bytes[32]={}; + assert(!volume.capacity()); + assert(!volume.plan_layout(true,100,0,100,false));assert(!volume.begin(100)); + assert(!volume.set_meta_size(8)&&!volume.write(0,bytes,32)&&!volume.read(0,bytes,32)); + assert(!volume.finalize()&&!volume.reopen()&&!volume.discard()); + volume.clear();volume.checkpoint();assert(!volume.staged_size()); + assert(!volume.approve_for_bootloader(bytes,bytes)); + assert(strstr(volume.last_error(),"source-only")); + assert(!card_queries);SdFs fs;volume._sd=&fs;volume._mounted=true; + mesh::ota::ready=true;assert(volume.capacity()==1048576&&card_queries==1); +} +''' + # No SdFat, GPIO, retained-memory or flash functions are declared: an + # accidental filesystem/hardware side effect is a compile failure. + run_native(self, code.replace("@CLASS@", cls).replace("@METHODS@", methods), + defines=FLAGS + ["OTA_SEEDER_ONLY"]) + + def test_actual_companion_eth_is_opt_in_and_busy_handoff_never_touches_spi(self): + stem = ROOT / "src/helpers/ethernet/RAK13800/RAK13800EthernetInterface" + cls = extract_braced(stem.with_suffix(".h").read_text(), "class RAK13800EthernetInterface") + ";" + cpp = stem.with_suffix(".cpp").read_text() + signatures = ("bool RAK13800EthernetInterface::begin()", "bool RAK13800EthernetInterface::beginHardware()", + "bool RAK13800EthernetInterface::tryEnable()", "bool RAK13800EthernetInterface::tryDisable()", + "bool RAK13800EthernetInterface::terminalShutdown(", "bool RAK13800EthernetInterface::shutdownForPowerOff(", + "void RAK13800EthernetInterface::disconnectClient()", "bool RAK13800EthernetInterface::isConnected() const", + "int RAK13800EthernetInterface::available()", "int RAK13800EthernetInterface::read()", + "size_t RAK13800EthernetInterface::write(", "void RAK13800EthernetInterface::loop()") + methods = "\n".join(extract_braced(cpp, name) for name in signatures) + methods = methods.replace("uint16_t remotePort = newClient.remotePort();", "(void)newClient.remotePort();") + source = SYNC.split("int main(", 1)[0] + source = source.replace("#include ", "#include \n#include ") + source = source.replace("void pinMode(int,int mode){if(mode==INPUT){assert(!spi);++pads;}}", "") + source = source.replace("void digitalWrite(int,int){}", "") + source = source.replace("struct SPIClass {void begin(){assert(!spi);spi=true;++begins;} void end(){assert(spi);spi=false;++ends;}} ETHERNET_SPI_PORT;", + "struct EthSpi{void begin(){assert(!spi);spi=true;++begins;}void end(){assert(spi);spi=false;++ends;}} ETHERNET_SPI_PORT;") + source = source.replace("void init(SPIClass&,int)", "void init(EthSpi&,int)") + source = source.replace("void disable(){disconnectClient();++cancels;}", "virtual void enable(){}\n virtual void disable(){disconnectClient();++cancels;}") + source = source.replace("@CLASS@", cls).replace("@METHODS@", methods) + source = source.replace("assert(spi);++closes;", "assert(spi&&!inserted);++closes;") + source += r''' +using namespace mesh::ota; +static bool busy=false,release_busy=false; +static unsigned prepared=0,released=0; +bool prepare(void*){++prepared;return !busy;} +bool canRelease(void*){return !release_busy;} +bool release(void*){assert(!spi&&!rak4631_ethernet_owns_spi());++released;return true;} +int main(){ + RAK13800EthernetInterface owner;assert(owner.begin());owner.enable(); + assert(!owner.hardwareActive()&&!spi&&!begins&&!owner.tryEnable()); + owner.setSpiOwnershipHooks(prepare,canRelease,release,nullptr); + inserted=true;assert(!owner.tryEnable()&&!prepared&&!begins); + inserted=false;busy=true;assert(!owner.tryEnable()&&prepared==1&&!begins); + busy=false;assert(rak4631_try_acquire_spi(RakSharedSpiOwner::Sd)); + assert(!owner.tryEnable()&&!begins&&rak4631_spi_owner()==RakSharedSpiOwner::Sd); + rak4631_release_spi(RakSharedSpiOwner::Sd); + assert(owner.tryEnable()&&owner.hardwareActive()&&rak4631_ethernet_owns_spi()); + release_busy=true;assert(!owner.tryDisable()&&owner.hardwareActive()&&ends==0); + release_busy=false;assert(owner.tryDisable()&&!owner.hardwareActive()&&ends==1&&closes==4); + assert(!rak4631_ethernet_owns_spi()&&released==2); + Ethernet.result=0;assert(!owner.tryEnable()&&!owner.hardwareActive()&&ends==2&&!rak4631_ethernet_owns_spi()); + Ethernet.result=1;assert(owner.tryEnable());inserted=true;const auto calls=reads; + assert(owner.available()==0&&owner.read()==-1&&owner.write(nullptr,0)==0&&reads==calls); + owner.loop();assert(!owner.hardwareActive()&&rak4631_ethernet_owns_spi()&&reads==calls); + assert(!owner.tryEnable());assert(owner.tryDisable()&&!rak4631_ethernet_owns_spi()); +} +''' + # The existing fixture's pad counter is irrelevant to the new mapped + # GPIO mock; ownership/SPI/socket traffic remains production code. + source = source.replace("ends=0,pads=0,cancels", "ends=0,cancels") + run_native(self, source) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_rak4631_rak15002_sd_recipes.py b/test/test_rak4631_rak15002_sd_recipes.py new file mode 100644 index 000000000..122c378df --- /dev/null +++ b/test/test_rak4631_rak15002_sd_recipes.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +"""Keep the RAK4631 adaptive RAK15002 build recipes scoped and bounded. + +These are recipe-only checks. They source shell helpers with PlatformIO +stubbed, so a configuration regression cannot quietly start a firmware build. +""" + +import configparser +import json +import os +from pathlib import Path +import re +import shlex +import shutil +import subprocess +import sys +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +ROOT_CONFIG = ROOT / "platformio.ini" +RAK_CONFIG = ROOT / "variants/rak4631/platformio.ini" +BUILD_SCRIPT = ROOT / "build_legacy.sh" +SEPARATOR = "\n__RAK15002_UNFLAGS__\n" + +STORE_DEFINES = ( + "OTA_RAK15002_SD_STORE=1", + "OTA_SD_STORE=1", + "OTA_SD_CS_PIN=26", + "OTA_SD_SCK_MHZ=4", + "OTA_SD_CACHE_MAX=16", + "OTA_MAX_SERVE=24", +) +RECEIVER_NOR_DEFINES = ( + "OTA_RAK_AUTO_STORE=1", + "OTA_QSPI_STORE=1", + "OTA_FLASH_STORE=1", + "OTA_QSPI_SHARED_WISBLOCK_SPI=1", + "OTA_QSPI_SCK_ARDUINO_PIN=3", + "OTA_QSPI_CS_ARDUINO_PIN=31", + "OTA_QSPI_IO0_ARDUINO_PIN=30", + "OTA_QSPI_IO1_ARDUINO_PIN=29", + "OTA_QSPI_IO2_NOT_CONNECTED=1", + "OTA_QSPI_IO3_NOT_CONNECTED=1", + "OTA_QSPI_SCK_FREQUENCY=NRF_QSPI_FREQ_32MDIV4", + "OTA_QSPI_EXPECTED_SIZE=2097152UL", +) +RECEIVERS = ( + "RAK_4631_repeater_unified_lora_ota", + "RAK_4631_repeater_lora_ota_no_external_sensors", + "RAK_4631_room_server_lora_ota_no_external_sensors", + "RAK_4631_room_server_ethernet_lora_ota_no_external_sensors", + "RAK_4631_sensor_lora_ota_no_external_sensors", +) +FULL_COMPANION = "RAK_4631_companion_radio_full" +INTERNAL_BOOTLOADER_TARGETS = ( + "RAK_4631_repeater_unified_lora_ota", + "RAK_4631_repeater_lora_ota_no_external_sensors", + "RAK_4631_room_server_lora_ota_no_external_sensors", + "RAK_4631_room_server_ethernet_lora_ota_no_external_sensors", + "RAK_4631_sensor_lora_ota_no_external_sensors", +) + + +def read_config(path): + config = configparser.ConfigParser(interpolation=None, inline_comment_prefixes=(";",)) + config.read(path, encoding="utf-8") + return config + + +def has_define(flags, define): + return re.search(r"(?:^|\s)-D\s*" + re.escape(define) + r"(?:\s|$)", flags) is not None + + +def run_shell(script): + # Windows' App Execution Alias can resolve `bash` to the WSL launcher, + # which accepts the command but loses shell-local assignments. Prefer the + # Git-for-Windows Bash used by the build scripts there; ordinary POSIX + # runners keep their PATH-provided Bash. + git_bash = Path(os.environ.get("ProgramFiles", r"C:\\Program Files")) / "Git/bin/bash.exe" + bash = str(git_bash) if os.name == "nt" and git_bash.is_file() else shutil.which("bash") + if not bash: + raise unittest.SkipTest("Bash is required for build recipe checks") + completed = subprocess.run( + [bash, "-c", script, "rak15002-recipe-test"], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + if completed.returncode: + raise AssertionError( + "recipe shell failed with %d:\nstdout:\n%s\nstderr:\n%s" + % (completed.returncode, completed.stdout, completed.stderr) + ) + return completed.stdout + + +def effective_flags(target): + """Exercise the real overlay order without consulting/running PlatformIO.""" + script = r''' +set -euo pipefail +source ./build_legacy.sh +target=@TARGET@ +# Every configuration query is deliberately false in this isolated test. The +# target-specific helper below must supply dynamic room/sensor OTA recipes. +pio_env_option_contains() { return 1; } +PIO_ENV_PLATFORM_BY_NAME[$target]=NRF52_PLATFORM +PIO_ENV_OTA_BY_NAME[$target]=1 +PIO_ENV_SD_OTA_BY_NAME[$target]=0 +PIO_ENV_QSPI_OTA_BY_NAME[$target]=0 +BUILD_PROFILE_EFFECTIVE=standard +BUILD_PROFILE_FOR_TARGET=standard +SKIP_DECLARED_REDUCTIONS=0 +NRF52_OTA_SENSOR_PROFILE=reduced +PLATFORMIO_BUILD_FLAGS=-DKEEP_RECIPE_FLAG=1 +PLATFORMIO_BUILD_UNFLAGS= +PLATFORMIO_BUILD_SRC_FILTER= +BUILD_REDUCTIONS=() +if is_companion_radio_full_target "$target"; then + apply_companion_radio_full_profile "$target" "$target" +else + apply_lora_ota_override "$target" +fi +apply_lora_ota_no_external_sensors_profile "$target" +apply_rak4631_rak15002_sd_store_profile "$target" +printf '%s\n__RAK15002_UNFLAGS__\n%s' "$PLATFORMIO_BUILD_FLAGS" "$PLATFORMIO_BUILD_UNFLAGS" +''' + result = run_shell(script.replace("@TARGET@", shlex.quote(target))).split(SEPARATOR) + if len(result) != 2: + raise AssertionError("unexpected shell recipe result: %r" % result) + return result + + +class Rak4631Rak15002SdRecipeTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.root = read_config(ROOT_CONFIG) + cls.rak = read_config(RAK_CONFIG) + + def assert_store_defines(self, flags): + for define in STORE_DEFINES: + with self.subTest(define=define): + self.assertTrue(has_define(flags, define), flags) + + def test_root_storage_helpers_pin_the_exact_optional_store(self): + store = self.root["nrf52_rak15002_sd_store"] + self.assert_store_defines(store["build_flags"]) + self.assertIn("SdFat-beta.git#a249cfa121e25a96ee14d8813abf01755f1f6a25", + store["lib_deps"]) + + unified = self.root["nrf52_rak4631_unified_ota"] + self.assertIn("${nrf52_wisblock_auto_ota.build_flags}", unified["build_flags"]) + self.assertIn("${nrf52_rak15002_sd_store.build_flags}", unified["build_flags"]) + + def test_static_repeater_and_full_companion_recipes_use_the_right_helper(self): + self.assertIn("${nrf52_rak4631_unified_ota.build_flags}", + self.rak["env:RAK_4631_repeater_unified_lora_ota"]["build_flags"]) + self.assertIn("${nrf52_rak4631_unified_ota.build_flags}", + self.rak["env:RAK_4631_repeater_lora_ota_no_external_sensors"] + ["build_flags"]) + + companion = self.rak["env:" + FULL_COMPANION] + self.assertIn("${nrf52_rak15002_sd_store.build_flags}", companion["build_flags"]) + for retained in ("ETHERNET_ENABLED=1", "ETHERNET_USE_RAK13800", + "ETHERNET_CLASS=RAK13800EthernetInterface", + "MESHCORE_REQUIRES_COMPANION_RADIO_FULL=1"): + with self.subTest(retained=retained): + self.assertIn(retained, companion["build_flags"]) + self.assertNotIn("OTA_HEAP_CONTEXT", companion["build_flags"]) + + # Generated room/sensor pairs compile their ordinary RAK4631 role + # environment. Keep the dependency scoped to the identities that can + # receive the STORE overlay instead of all RAK4631 roles. + for section, helper in ( + ("env:RAK_4631_repeater_unified_lora_ota", + "${nrf52_rak4631_unified_ota.lib_deps}"), + ("env:RAK_4631_repeater_lora_ota_no_external_sensors", + "${nrf52_rak4631_unified_ota.lib_deps}"), + ("env:RAK_4631_room_server", "${nrf52_rak15002_sd_store.lib_deps}"), + ("env:RAK_4631_room_server_ethernet", + "${nrf52_rak15002_sd_store.lib_deps}"), + ("env:RAK_4631_sensor", "${nrf52_rak15002_sd_store.lib_deps}"), + ("env:" + FULL_COMPANION, "${nrf52_rak15002_sd_store.lib_deps}")): + with self.subTest(section=section): + self.assertIn(helper, + self.rak[section]["lib_deps"]) + + def test_legacy_cache_only_seeder_fences_off_adaptive_receiver_flags(self): + seeder = self.rak["env:RAK_4631_repeater_rak15002_sd_seeder_only"] + flags = seeder["build_flags"] + self.assertTrue(has_define(flags, "OTA_RAK15002_SD_CACHE=1")) + self.assertTrue(has_define(flags, "OTA_HEAP_CONTEXT=1")) + self.assertNotIn("OTA_RAK15002_SD_STORE", flags) + unflags = seeder["build_unflags"] + self.assertIn("${nrf52_base.build_unflags}", unflags) + for define in ("OTA_RAK_AUTO_STORE=1", "OTA_QSPI_STORE=1", + "OTA_RAK15002_SD_STORE=1"): + with self.subTest(define=define): + self.assertTrue(has_define(unflags, define), unflags) + + def test_all_canonical_receiver_pair_identities_gain_adaptive_store_flags(self): + for target in RECEIVERS: + with self.subTest(target=target): + flags, unflags = effective_flags(target) + self.assert_store_defines(flags) + for define in RECEIVER_NOR_DEFINES: + self.assertTrue(has_define(flags, define), flags) + self.assertNotIn("OTA_SEEDER_ONLY", flags) + self.assertNotIn("OTA_HEAP_CONTEXT", flags) + self.assertNotIn("OTA_RAK15002_SD_STORE", unflags) + + def test_full_companion_is_sd_source_only_without_losing_existing_features(self): + flags, unflags = effective_flags(FULL_COMPANION) + self.assert_store_defines(flags) + for define in ("OTA_SEEDER_ONLY=1", "COMPANION_RADIO_FULL=1", + "COMPANION_FEATURE_TEMP_RADIO=1", "COMPANION_FEATURE_OTA_CLI=1", + "OTA_SHARED_COMPANION_QUEUE=1"): + with self.subTest(define=define): + self.assertTrue(has_define(flags, define), flags) + self.assertNotIn("OTA_RAK_AUTO_STORE", flags) + self.assertNotIn("OTA_QSPI_STORE", flags) + self.assertNotIn("OTA_HEAP_CONTEXT", flags) + self.assertNotIn("OTA_RAK15002_SD_STORE", unflags) + self.assertNotIn("OTA_SD_STORE=1", unflags) + + def test_noncanonical_or_legacy_targets_do_not_receive_store_overlay(self): + script = r''' +set -euo pipefail +source ./build_legacy.sh +target=@TARGET@ +PIO_ENV_PLATFORM_BY_NAME[$target]=NRF52_PLATFORM +PLATFORMIO_BUILD_FLAGS=-DKEEP_RECIPE_FLAG=1 +apply_rak4631_rak15002_sd_store_profile "$target" +printf '%s' "$PLATFORMIO_BUILD_FLAGS" +''' + for target in ( + "RAK_4631_repeater", "RAK_4631_room_server", "RAK_4631_sensor", + "RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors", + "RAK_4631_repeater_rak15001_slot_c_lora_ota", + "RAK_4631_repeater_rak15002_sd_seeder_only", + "RAK_4631_companion_radio_usb"): + with self.subTest(target=target): + flags = run_shell(script.replace("@TARGET@", shlex.quote(target))) + self.assertEqual(flags, "-DKEEP_RECIPE_FLAG=1") + + def test_qualified_repeater_keeps_hybrid_bootloader_linker_request(self): + script = r''' +set -euo pipefail +source ./build_legacy.sh +target=RAK_4631_repeater_lora_ota_no_external_sensors +PIO_ENV_PLATFORM_BY_NAME[$target]=NRF52_PLATFORM +PIO_ENV_QSPI_OTA_BY_NAME[$target]=1 +PIO_ENV_SD_OTA_BY_NAME[$target]=0 +is_lora_ota_build() { return 0; } +pio_env_option_contains() { + [ "$1" = "$target" ] && [ "$2" = build_flags ] && [ "$3" = OTA_RAK_AUTO_STORE ] +} +unset MESHCORE_NRF52_INTERNAL_BOOTLOADER_UPDATE +apply_nrf52_lora_ota_build_recipe "$target" "$target" +printf '%s' "${MESHCORE_NRF52_INTERNAL_BOOTLOADER_UPDATE:-0}" +''' + # The pre-script converts this request into OTA_INTERNAL_BOOTLOADER_UPDATE + # and the nrf52840_s140_v6_mota64 linker. The optional RAK15002 must + # not demote this already-qualified internal/NOR receiver contract. + self.assertEqual(run_shell(script), "1") + + def test_dynamic_sd_receivers_keep_the_frozen_hybrid_request_before_overlay(self): + """Synthetic role aliases must request mota64 before STORE flags exist.""" + inventory = { + line.strip() + for line in (ROOT / "tools/mota/nrf52_internal_bootloader_targets.txt") + .read_text(encoding="utf-8").splitlines() + if line.strip() and not line.lstrip().startswith("#") + } + self.assertTrue(set(INTERNAL_BOOTLOADER_TARGETS).issubset(inventory)) + + script = r''' +set -euo pipefail +source ./build_legacy.sh +target=@TARGET@ +# These aliases resolve to an ordinary room/sensor PlatformIO base. At the +# hybrid-linker decision point it has neither AUTO nor STORE flags yet. +PIO_ENV_PLATFORM_BY_NAME[$target]=NRF52_PLATFORM +PIO_ENV_QSPI_OTA_BY_NAME[$target]=0 +PIO_ENV_SD_OTA_BY_NAME[$target]=0 +PIO_ENV_OTA_BY_NAME[$target]=1 +NRF52_OTA_SENSOR_PROFILE=reduced +BUILD_PROFILE_EFFECTIVE=standard +BUILD_PROFILE_FOR_TARGET=standard +pio_env_option_contains() { return 1; } +unset MESHCORE_NRF52_INTERNAL_BOOTLOADER_UPDATE +apply_nrf52_lora_ota_build_recipe "$target" "$target" +printf '%s' "${MESHCORE_NRF52_INTERNAL_BOOTLOADER_UPDATE:-0}" +''' + for target in INTERNAL_BOOTLOADER_TARGETS[2:]: + with self.subTest(target=target): + # The pre-script turns this request into the exact existing + # nrf52840_s140_v6_mota64 linker plus both hybrid flags. The + # later optional SD overlay must never weaken that contract. + self.assertEqual( + run_shell(script.replace("@TARGET@", shlex.quote(target))), "1") + + def test_adaptive_store_is_not_classified_as_an_sd_primary_profile(self): + source = BUILD_SCRIPT.read_text(encoding="utf-8") + match = re.search( + r"python3 -c '\n(.*?)\n' \"\$SUPPORTED_PLATFORM_PATTERN\"", + source, + flags=re.DOTALL, + ) + self.assertIsNotNone(match, "build context parser missing") + config = [ + ["env:RAK_4631_repeater_lora_ota_no_external_sensors", [ + ["board", "rak4631"], + ["build_flags", ["-D NRF52_PLATFORM", "-D OTA_RAK_AUTO_STORE=1", + "-D OTA_QSPI_STORE=1", "-D OTA_SD_STORE=1", + "-D OTA_RAK15002_SD_STORE=1"]], + ["build_src_filter", ["+"]], + ]], + ["env:RAK_4631_repeater_rak15002_sd_seeder_only", [ + ["board", "rak4631"], + ["build_flags", ["-D NRF52_PLATFORM", "-D OTA_SD_STORE=1", + "-D OTA_RAK15002_SD_CACHE=1"]], + ["build_src_filter", ["+"]], + ]], + ] + result = subprocess.run( + [sys.executable, "-c", match.group(1), "ESP32_PLATFORM|NRF52_PLATFORM"], + input=json.dumps(config), text=True, capture_output=True, check=False, + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + rows = {line.split("\t", 1)[0]: line.split("\t") + for line in result.stdout.strip().splitlines()} + receiver = rows["RAK_4631_repeater_lora_ota_no_external_sensors"] + cache = rows["RAK_4631_repeater_rak15002_sd_seeder_only"] + self.assertEqual((receiver[4], receiver[5]), ("0", "1")) + self.assertEqual(cache[4], "1") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_rak_combined_ota.py b/test/test_rak_combined_ota.py index 469dfb4f8..fad0c312a 100644 --- a/test/test_rak_combined_ota.py +++ b/test/test_rak_combined_ota.py @@ -78,7 +78,7 @@ int main() { def test_context_handoff_frees_idle_self_serve_and_preserves_busy_ota(self): header = (ROOT / "src/helpers/ota/OtaContext.h").read_text() start = header.index(" // Only the main thread may change ownership.") - end = header.index("#endif\n\n#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)", start) + end = header.index("#endif\n\n#if defined(OTA_RAK15002_SD_STORE)", start) body = header[start:end] source = r''' #include diff --git a/test/test_rak_sd_context_handoff.py b/test/test_rak_sd_context_handoff.py new file mode 100644 index 000000000..b078d3c62 --- /dev/null +++ b/test/test_rak_sd_context_handoff.py @@ -0,0 +1,183 @@ +"""Execute the Full RAK SD/Ethernet context handoff without hardware.""" +from pathlib import Path +import unittest + +from test_rak15002_sd_seeder import build +from test_replay_reset_integration import extract_braced + +ROOT = Path(__file__).resolve().parents[1] +HARNESS = r''' +#include +#include +#include +#include +struct OtaManager { enum { IDLE, FETCHING, COMPLETE, PAUSED }; }; +struct Store { + uint32_t staged = 0; + unsigned releases = 0, selections = 0; + uint32_t staged_size() const { return staged; } + bool releaseBus() { ++releases; return staged == 0; } + bool resetSelection() { ++selections; return staged == 0; } +}; +struct Cache { unsigned resets = 0; void resetMedia() { ++resets; } }; +struct Manager { + int state = OtaManager::IDLE; + bool own = true, interest = true, accept_full = true; + unsigned serve_jobs = 0, manifest_jobs = 0, removals = 0, clears = 0; + bool servingPrimaryManifest(const uint8_t*) const { return own; } + int fetchState() const { return state; } + unsigned pendingServeJobs() const { return serve_jobs; } + unsigned pendingManifestJobs() const { return manifest_jobs; } + void remove_source(Cache*) { ++removals; } + void set_accept_full(bool value) { accept_full = value; } + void set_archive_interest(bool value) { interest = value; } + void clear_primary() { ++clears; } +}; +struct Context { + Manager manager; + Store fetch_store, card; + Cache sd_cache; + uint8_t serve_self_manifest[1] = {}; + bool apply_pending = false, bootloader_apply_pending = false, _sd_cache_fetching = false; + bool serving = true, folder_active = false, fetch_to_folder = false; + bool _sd_cache_source_attached = true; + void* _folder_source = nullptr; + void* folder_dest = nullptr; + void* disconnected_folder_dest = nullptr; + uint32_t serve_expected = 0, _sd_cache_init_retry_at = 999; + uint8_t* serve_self_leaves = nullptr; + uint8_t* serve_self_proof = nullptr; + unsigned buffer_releases = 0; + Store& sdStagingStore() { return card; } + void releaseServeBuffer() { ++buffer_releases; } +@METHODS@ +}; +int main() { + for (unsigned busy = 0; busy < 14; ++busy) { + Context c; + switch (busy) { + case 0: c.manager.state = OtaManager::FETCHING; break; + case 1: c.manager.state = OtaManager::COMPLETE; break; + case 2: c.manager.state = OtaManager::PAUSED; break; + case 3: c.fetch_store.staged = 1; break; + case 4: c.apply_pending = true; break; + case 5: c.bootloader_apply_pending = true; break; + case 6: c._sd_cache_fetching = true; break; + case 7: c.manager.serve_jobs = 1; break; + case 8: c.manager.manifest_jobs = 1; break; + case 9: c.manager.own = false; break; + case 10: c.serve_expected = 1; break; + case 11: c.folder_active = true; break; + case 12: c._folder_source = &c; break; + case 13: c.folder_dest = &c; break; + } + assert(!c.canReleaseRakSdForEthernet()); + assert(!c.prepareRakSdForEthernet() && !c.releasedRakSdEthernet()); + assert(c.sd_cache.resets == 0 && c.card.releases == 0); + assert(c.manager.removals == 0 && c.manager.clears == 0 && c.manager.interest); + } + for (bool disconnected : {false, true}) { + Context c; + if (disconnected) c.disconnected_folder_dest = &c; + else c.fetch_to_folder = true; + assert(!c.prepareRakSdForEthernet() && c.card.releases == 0); + } + Context c; + c.serve_self_leaves = static_cast(malloc(8)); + c.serve_self_proof = static_cast(malloc(8)); + assert(c.canReleaseRakSdForEthernet() && c.prepareRakSdForEthernet()); + assert(c.card.releases == 1 && c.sd_cache.resets == 1); + assert(!c._sd_cache_source_attached && c.manager.removals == 1); + assert(!c.serving && !c.serve_self_leaves && !c.serve_self_proof && c.buffer_releases == 1); + assert(!c.manager.interest && c._sd_cache_init_retry_at == 0); +#ifdef OTA_SEEDER_ONLY + assert(c.manager.accept_full && c.fetch_store.selections == 0); +#else + assert(!c.manager.accept_full && c.fetch_store.selections == 1); +#endif + assert(c.releasedRakSdEthernet() && c.manager.interest); + assert(c.card.releases == 1); // release callback never probes or remounts +} +''' + + +class RakSdContextHandoffTests(unittest.TestCase): + def test_actual_idle_handoff_and_every_pinned_work_guard(self): + source = (ROOT / 'src/helpers/ota/OtaContext.h').read_text() + methods = '\n'.join(extract_braced(source, signature) for signature in ( + 'bool canReleaseRakSdForEthernet() const', 'bool prepareRakSdForEthernet()', + 'bool releasedRakSdEthernet()')) + harness = HARNESS.replace('@METHODS@', methods) + for seeder in (False, True): + with self.subTest(seeder=seeder): + build(self, harness, ['OTA_RAK15002_SD_STORE', + *(['OTA_SEEDER_ONLY'] if seeder else [])]) + + def test_companion_physical_ethernet_and_sd_commands_keep_failed_admission_off(self): + source = (ROOT / 'examples/companion_radio/main.cpp').read_text() + method = extract_braced(source, 'bool handleRak4631PeripheralCommand(') + fixture = r''' +#include +#include +#include +#include +struct Board {} board; +struct Ethernet { + bool active = false, allowed = true; + unsigned enables = 0, disables = 0; + bool tryEnable() { ++enables; if (!allowed) return false; active = true; return true; } + bool tryDisable() { ++disables; if (!allowed) return false; active = false; return true; } + bool hardwareActive() const { return active; } + const char* last_error() const { return "storage busy"; } +} ethernet_interface; +namespace mesh { namespace ota { +bool admission = true; +unsigned acquires = 0; +struct Card { + unsigned queries = 0; + bool okay = true; + bool getSpace(Board&, uint64_t& used, uint64_t& free) { + ++queries; used = 42; free = 4096; return okay; + } + const char* last_error() const { return "no SD card"; } +}; +struct Context { Card card; Card& sdStagingStore() { return card; } } context; +Context& ota_ctx() { assert(admission); return context; } +bool ota_acquire_context(char* reply, size_t cap) { + ++acquires; if (!admission) snprintf(reply, cap, "Error: queue busy"); return admission; +} +} } +@METHOD@ +int main() { + char reply[160]; + assert(handleRak4631PeripheralCommand("get eth", reply, sizeof reply)); + assert(strstr(reply, "eth=off") && !ethernet_interface.enables); + assert(!handleRak4631PeripheralCommand("get unrelated", reply, sizeof reply)); + assert(handleRak4631PeripheralCommand("set eth both", reply, sizeof reply)); + assert(strstr(reply, "Error") && !ethernet_interface.enables); + ethernet_interface.allowed = false; + assert(handleRak4631PeripheralCommand("set eth on", reply, sizeof reply)); + assert(strstr(reply, "storage busy") && !ethernet_interface.active); + ethernet_interface.allowed = true; + assert(handleRak4631PeripheralCommand("set eth on", reply, sizeof reply)); + assert(strstr(reply, "eth=on") && ethernet_interface.active); + assert(handleRak4631PeripheralCommand("get sdcard free", reply, sizeof reply)); + assert(strstr(reply, "Ethernet off") && !mesh::ota::acquires); + assert(handleRak4631PeripheralCommand("set eth off", reply, sizeof reply)); + assert(!ethernet_interface.active); + mesh::ota::admission = false; + assert(handleRak4631PeripheralCommand("get sdcard", reply, sizeof reply)); + assert(strstr(reply, "queue busy") && !mesh::ota::context.card.queries); + mesh::ota::admission = true; + assert(handleRak4631PeripheralCommand("get sdcard free", reply, sizeof reply)); + assert(strstr(reply, "used=42 free=4096 bytes") && mesh::ota::context.card.queries == 1); + mesh::ota::context.card.okay = false; + assert(handleRak4631PeripheralCommand("get sdcard", reply, sizeof reply)); + assert(strstr(reply, "no SD card")); +} +'''.replace('@METHOD@', method) + build(self, fixture) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_rak_sd_receiver.py b/test/test_rak_sd_receiver.py new file mode 100644 index 000000000..4bde5208c --- /dev/null +++ b/test/test_rak_sd_receiver.py @@ -0,0 +1,535 @@ +"""Native regression for the exact RAK15002 receiver capability and selector.""" + +from pathlib import Path +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +def method(source, signature): + start = source.index(signature) + opening = source.index("{", start) + depth, end = 1, opening + 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +def run_cpp(source, defines=()): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + cpp = path / "test.cpp" + cpp.write_text(source, encoding="utf-8") + exe = path / "test" + subprocess.run([ + "c++", "-std=c++17", "-Wall", "-Wextra", "-Werror", + *("-D" + value for value in defines), "-I", str(ROOT), + str(cpp), "-o", str(exe), + ], check=True, timeout=30) + subprocess.run([str(exe)], check=True, timeout=10) + + +class RakSdReceiverTest(unittest.TestCase): + def test_actual_apply_requires_exact_sd_loader_and_selects_runtime_handoff(self): + apply = (ROOT / "src/helpers/ota/OtaApply.cpp").read_text() + functions = "\n".join(method(apply, signature) for signature in ( + "bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store", + "bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store", + "void ota_reboot_to_apply() { // public:", + )) + run_cpp(r''' +#include +#include +#include "src/helpers/ota/OtaBlInfo.h" +#include "src/helpers/ota/OtaFlashLayout_nrf52.h" +using namespace mesh::ota; +namespace mesh { namespace ota { +static bool identity_ok = true, verified = true, clear_ok = true; +static const char* identity_name = "4631_DFU"; +static unsigned verification_calls = 0, resets = 0; +static uint8_t request = 0, source = 0; +static OtaBlCaps caps; +struct OtaBootloaderIdentity { bool crc_ok = true; const char* device_name; }; +bool ota_installed_bootloader_identity(OtaBootloaderIdentity& out) { + out.device_name = identity_name; return identity_ok; +} +OtaBlCaps appCaps() { return caps; } +#define ota_bootloader_app_caps appCaps +struct OtaStoreFlashNrf52 { + bool hybrid = false; + bool is_hybrid() const { return hybrid; } + bool publish_hybrid_handoff() { return true; } +}; +static OtaStoreFlashNrf52* g_nrf52_apply_store = nullptr; +static uint8_t g_nrf52_external_handoff = GPREGRET2_OTA_STAGE_QSPI; +struct OtaStoreSdNrf52 {}; +struct OtaStoreQspiNrf52 { + static bool headerW25Detected() { return false; } + uint32_t jedec_id() const { return 0xef4015; } +}; +struct SignerAllowlist {}; +struct ApplyState {}; +template bool ota_apply_mota_nrf52_external( + Store&, const SignerAllowlist&, uint8_t flag, const char*, bool trusted, + ApplyState&, char*) { + ++verification_calls; + assert(trusted == (flag == OTA_BL_STORAGE_SD)); + return verified; +} +void ota_nrf52_set_reset_handoff(uint8_t req, uint8_t src) { request = req; source = src; } +bool ota_nrf52_clear_reset_reasons() { return clear_ok; } +void NVIC_SystemReset() { ++resets; } +uint32_t simulatedCeiling() { return MOTA_NRF52_STAGE_CEILING_EXPANDED; } +#define ota_nrf52_effective_stage_ceiling simulatedCeiling +''' + functions + r''' +} } +int main() { + OtaStoreSdNrf52 sd; OtaStoreQspiNrf52 nor; OtaStoreFlashNrf52 internal; + SignerAllowlist allow; ApplyState state; char msg[160]; + caps.optional_sd_app_storage = 0; + g_nrf52_apply_store = &internal; + assert(!ota_apply_mota_nrf52(sd, allow, state, msg)); + assert(!g_nrf52_apply_store && verification_calls == 0); + caps.optional_sd_app_storage = OTA_BL_STORAGE_SD; identity_ok = false; + assert(!ota_apply_mota_nrf52(sd, allow, state, msg) && verification_calls == 0); + identity_ok = true; identity_name = "3401_DFU"; + assert(!ota_apply_mota_nrf52(sd, allow, state, msg) && verification_calls == 0); + identity_name = "4631_DFU"; verified = false; + assert(!ota_apply_mota_nrf52(sd, allow, state, msg)); + assert(verification_calls == 1 && g_nrf52_external_handoff == GPREGRET2_OTA_STAGE_QSPI); + verified = true; + assert(ota_apply_mota_nrf52(sd, allow, state, msg)); + ota_reboot_to_apply(); + assert(request == GPREGRET_OTA_APPLY && source == GPREGRET2_OTA_STAGE_SD && resets == 1); + assert(ota_apply_mota_nrf52(nor, allow, state, msg)); + ota_reboot_to_apply(); + assert(request == GPREGRET_OTA_APPLY && source == GPREGRET2_OTA_STAGE_QSPI && resets == 2); + g_nrf52_apply_store = &internal; + ota_reboot_to_apply(); + assert(source == GPREGRET2_OTA_STAGE_EXPANDED && resets == 3); + internal.hybrid = true; + ota_reboot_to_apply(); + assert(source == GPREGRET2_OTA_STAGE_HYBRID && resets == 4); + clear_ok = false; + ota_reboot_to_apply(); + assert(request == 0 && source == 0xbd && resets == 5); +} +''', ("OTA_RAK15002_SD_STORE", "OTA_RAK_AUTO_STORE", "OTA_SD_STORE", "OTA_QSPI_STORE")) + + def test_actual_pull_gate_distinguishes_sd_nor_and_internal_boot_packages(self): + cli = (ROOT / "src/helpers/ota/OtaCli.cpp").read_text() + error = cli.index("ERR bootloader cannot apply from detected QSPI storage") + start = cli.rfind("#elif defined(OTA_RAK_AUTO_STORE)", 0, error) + end = cli.index("#elif defined(OTA_QSPI_STORE)", error) + gate = cli[cli.index("\n", start) + 1:end] + run_cpp(r''' +#include +#include +#include +#include +constexpr uint8_t OTA_BL_STORAGE_QSPI = 4, OTA_BL_STORAGE_SD = 1, MFLAG_SIGNED = 2; +struct Store { + int mode; + bool usesExternal() const { return mode == 1 || mode == 2; } + bool usesQspi() const { return mode == 1; } + bool usesSd() const { return mode == 2; } + bool usesInternal() const { return mode == 0; } + const char* selectionReason() const { return "not prepared"; } +}; +bool rejects(bool selboot, int medium, uint8_t flags, uint8_t sd, uint8_t selflags = 2) { + struct { Store fetch_store; } c = {{medium}}; + struct { uint8_t storage_flags, optional_sd_app_storage; } bl = {flags, sd}; + char reply[160]; +''' + gate + r''' + return false; +} +int main() { + assert(!rejects(false, 2, 0x1f, 1)); + assert(rejects(false, 2, 0x1e, 0)); + assert(rejects(false, 2, 0x1f, 1, 0)); + assert(!rejects(false, 1, 0x1e, 0)); + assert(rejects(false, 1, 0x0a, 0)); + assert(!rejects(false, 0, 0x0a, 0)); + assert(rejects(false, 3, 0x1f, 1)); + for (int medium = 0; medium < 4; ++medium) + assert(!rejects(true, medium, 0x0a, 0, 0)); // privileged path is internal, never SD +} +''', ("OTA_RAK15002_SD_STORE",)) + + def test_layout_and_status_dispatch_do_not_prepare_optional_storage(self): + layout = (ROOT / "src/helpers/StorageLayout.cpp").read_text() + start = layout.index("#if defined(ENABLE_OTA) && defined(OTA_RAK15002_SD_STORE)") + end = layout.index("#elif defined(ENABLE_OTA) && defined(OTA_RAK_AUTO_STORE)", start) + body = layout[layout.index("\n", start) + 1:end] + self.assertNotIn("getSpace(", body) + self.assertNotIn("ota_acquire_context", body) + self.assertNotIn("ota_ctx()", body) + run_cpp(r''' +#include +#include +#include +#include +namespace mesh { namespace ota { +struct Store { + int medium = -1; + const char* selectionReason() const { return medium < 0 ? "not probed" : "selected"; } + bool usesSd() const { return medium == 2; } + bool usesQspi() const { return medium == 1; } + bool usesInternal() const { return medium == 0; } + unsigned staged_size() const { return 0; } +}; +struct Context { Store fetch_store; }; +static Context* active = nullptr; +const Context* ota_context_if_active() { return active; } +} } +void appendStorageLayout(char* reply, size_t reply_size, const char* format, ...) { + va_list args; va_start(args, format); + vsnprintf(reply + strlen(reply), reply_size - strlen(reply), format, args); + va_end(args); +} +void status(char* reply) { + int board = 0; size_t reply_size = 160; + reply[0] = 0; +''' + body + r''' +} +int main() { + char reply[160]; + status(reply); assert(strstr(reply, "not-prepared")); + mesh::ota::Context context; mesh::ota::active = &context; + status(reply); +#if defined(OTA_SEEDER_ONLY) + assert(strstr(reply, "source-only")); +#else + assert(strstr(reply, "not-prepared")); + for (int medium = 0; medium < 3; ++medium) { + context.fetch_store.medium = medium; + status(reply); + assert(strstr(reply, medium == 0 ? "medium=internal" : medium == 1 ? "medium=qspi" : "medium=sd")); + } +#endif +} +''') + # Full Companion uses the separate stable SD archive, not an adaptive + # receiver. Compile the same production status block in both roles. + run_cpp(r''' +#include +#include +#include +#include +namespace mesh { namespace ota { +struct Context {}; +const Context* ota_context_if_active() { static Context c; return &c; } +} } +void appendStorageLayout(char* reply, size_t reply_size, const char* format, ...) { + va_list args; va_start(args, format); + vsnprintf(reply + strlen(reply), reply_size - strlen(reply), format, args); + va_end(args); +} +int main() { + int board = 0; size_t reply_size = 160; char reply[160] = {}; +''' + body + r''' + assert(strstr(reply, "source-only")); +} +''', ("OTA_SEEDER_ONLY",)) + cli = (ROOT / "src/helpers/ota/OtaCli.cpp").read_text() + dispatcher = method(cli, "bool handle_ota_command(") + self.assertLess(dispatcher.index("return handle_status(reply, c)"), + dispatcher.index("c.prepareRakStorage()")) + self.assertIn('"qspi|storage|self|id|stats|help|?|h"', dispatcher) + + def test_existing_local_combined_loader_advertises_the_exact_new_capability(self): + candidate = (ROOT.parent / "Adafruit_nRF52_Bootloader_OTAFIX" / + "_build-rak4631-all-in-one-test-20261015/cmake/bootloader.bin") + if not candidate.is_file(): + self.skipTest("Optional previously-built OTAFIX qualification candidate is absent") + # Read-only admission of an existing ARM binary, not a bootloader build + # or firmware upload. CI need not have the separate loader repository. + image = candidate.read_bytes() + self.assertEqual(len(image), 0xA000) + source = r''' +#include +#include "src/helpers/ota/OtaBlInfo.h" +using namespace mesh::ota; +static const uint8_t image[] = { @IMAGE@ }; +int main() { + const auto app = ota_bl_app_caps_scan(image, sizeof(image)); + const auto boot = ota_bl_update_caps_scan_aligned(image, sizeof(image), 0x0a); + assert(app.present && app.storage_flags == 0x1f && app.optional_app_storage == 0x14); + assert(app.optional_sd_app_storage == 1); + assert(boot.present && boot.storage_flags == 0x0a && boot.optional_sd_app_storage == 1); +} +'''.replace("@IMAGE@", ",".join(str(value) for value in image)) + run_cpp(source) + + def test_memory_mapped_capabilities_are_scoped_to_rak4631(self): + source = r''' +#include +#include +#include +#include "src/helpers/ota/OtaBlInfo.h" +using namespace mesh::ota; +int main() { + auto* flash = static_cast(mmap(reinterpret_cast(uintptr_t(0xf4000)), + 0xa000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0)); + assert(flash == reinterpret_cast(uintptr_t(0xf4000))); + memset(flash, 0, 0xa000); + const uint8_t records[] = { + 'M','O','T','A','B','L','D','R',3,0,5,0,0x0a,0,0,0, + 'M','O','T','A','R','A','M','A',1,0,72,0,0,0,1,0, + 'M','O','T','A','S','T','O','R',1,0,16,0,0x14,0,0,0, + 'M','O','T','A','S','D','R','A',1,0,72,0,1,0x53,0,0, + }; + memcpy(flash, records, sizeof(records)); + auto app = ota_bootloader_app_caps(); + auto boot = ota_bootloader_update_caps(); +#if defined(RAK_4631) || defined(OTA_RAK15002_SD_STORE) + assert(app.storage_flags == 0x1f && app.optional_sd_app_storage == 1); + assert(boot.storage_flags == 0x0a && boot.optional_sd_app_storage == 1); +#else + assert(app.storage_flags == 0x1e && app.optional_sd_app_storage == 0); + assert(boot.storage_flags == 0x0a && boot.optional_sd_app_storage == 0); +#endif + assert(ota_bootloader_self_update_caps_valid(boot)); + assert(munmap(flash, 0xa000) == 0); +} +''' + for scope in ((), ("RAK_4631",), ("OTA_RAK15002_SD_STORE",)): + with self.subTest(scope=scope): + run_cpp(source, ("NRF52_PLATFORM", "OTA_RAK_AUTO_STORE", + "OTA_INTERNAL_BOOTLOADER_UPDATE", *scope)) + + def test_exact_supplement_preserves_legacy_application_and_update_views(self): + run_cpp(r''' +#include +#include +#include +#include "src/helpers/ota/OtaBlInfo.h" +using namespace mesh::ota; +static const uint8_t boot[16] = { + 'M','O','T','A','B','L','D','R',3,0,5,0,0x0a,0,0,0, +}; +static const uint8_t bundle[32] = { + 'M','O','T','A','R','A','M','A',1,0,72,0,0,0,1,0, + 'M','O','T','A','S','T','O','R',1,0,16,0,0x14,0,0,0, +}; +static const uint8_t sd[16] = { + 'M','O','T','A','S','D','R','A',1,0,72,0,1,0x53,0,0, +}; +static void make(uint8_t* image) { + memset(image, 0, 256); + memcpy(image, boot, sizeof(boot)); + memcpy(image + 32, bundle, sizeof(bundle)); + memcpy(image + 80, sd, sizeof(sd)); +} +static void check(const uint8_t* image, uint8_t expected) { + auto app = ota_bl_app_caps_scan(image, 256); + auto update = ota_bl_update_caps_scan_aligned(image, 256, 0x0a); + assert(app.present && app.storage_flags == (0x1e | expected)); + assert(app.optional_app_storage == 0x14 && app.optional_sd_app_storage == expected); + assert(update.present && update.storage_flags == 0x0a); + assert(update.optional_app_storage == 0x14 && update.optional_sd_app_storage == expected); +} +int main() { + uint8_t image[256]; + make(image); check(image, 1); + // Old loaders retain exactly their old internal/NOR contract. + memset(image + 80, 0, sizeof(sd)); check(image, 0); + for (unsigned i = 0; i < sizeof(sd); ++i) { + make(image); image[80 + i] ^= 1; check(image, 0); + } + make(image); memcpy(image + 112, sd, sizeof(sd)); check(image, 0); + make(image); memcpy(image + 112, sd, sizeof(sd)); image[125] ^= 1; check(image, 0); + make(image); memcpy(image + 248, sd, 8); check(image, 0); // truncated duplicate + make(image); memmove(image + 81, image + 80, sizeof(sd)); image[80] = 0; check(image, 0); + make(image); + assert(ota_bl_rak_sd_app_storage(image, 95, 0x0a, 0x14) == 0); + assert(ota_bl_rak_sd_app_storage(nullptr, 256, 0x0a, 0x14) == 0); + for (uint8_t primary : {uint8_t(9), uint8_t(0x0e), uint8_t(0x1e)}) { + make(image); image[12] = primary; + auto app = ota_bl_app_caps_scan(image, sizeof(image)); + assert(app.optional_sd_app_storage == 0 && app.storage_flags == primary); + } + // Missing/corrupt retained-RAM/NOR metadata cannot borrow the SD marker. + for (unsigned i = 0; i < sizeof(bundle); ++i) { + make(image); image[32 + i] ^= 1; + assert(ota_bl_app_caps_scan(image, sizeof(image)).optional_sd_app_storage == 0); + } + make(image); memcpy(image + 128, boot, sizeof(boot)); + assert(!ota_bl_update_caps_scan_aligned(image, sizeof(image), 0x0a).present); +} +''') + + def test_actual_adaptive_store_defers_io_and_preserves_each_owner(self): + header = (ROOT / "src/helpers/ota/OtaStoreAdaptiveNrf52.h").read_text() + body = header[header.index("class OtaStoreAdaptiveNrf52"): + header.index("} // namespace ota")] + source = r''' +#include +#include +#include +#include "src/helpers/ota/OtaStore.h" +#include "src/helpers/ota/OtaBlInfo.h" +#include "src/helpers/ota/OtaRakStoragePolicy.h" +#include "src/helpers/nrf52/Rak4631SharedSpi.h" +using namespace mesh::ota; +namespace mesh { namespace ota { +static unsigned probes = 0, card_checks = 0, sd_probes = 0; +static unsigned internal_live = 0, nor_live = 0; +static bool card = true, media = true, sd_cap = true, valid_identity = true; +static bool release_ok = true; +static uint8_t detected = 2; +static const char* device_name = "4631_DFU"; +struct OtaBootloaderIdentity { bool crc_ok = true; const char* device_name; }; +bool ota_installed_bootloader_identity(OtaBootloaderIdentity& out) { + out.device_name = device_name; return valid_identity; +} +struct MotaManifest { bool is_bootloader() const { return false; } }; +bool mota_parse_manifest(const uint8_t*, size_t, MotaManifest&) { return false; } +OtaBlCaps appCaps() { + OtaBlCaps c; c.present = true; c.apply_abi = 3; c.codec_mask = 5; + c.storage_flags = sd_cap ? 0x1f : 0x1e; c.optional_app_storage = 0x14; + c.optional_sd_app_storage = sd_cap ? 1 : 0; return c; +} +OtaBlCaps bootCaps() { auto c = appCaps(); c.storage_flags = 0x0a; return c; } +bool ota_rak15002_sd_card_present() { + assert(!rak4631_ethernet_owns_spi()); ++card_checks; return card; +} +class Store : public OtaStore { +public: + uint32_t size = 0; + bool begin(uint32_t n) override { size = n; return true; } + bool write(uint32_t, const uint8_t*, uint32_t) override { return true; } + bool read(uint32_t, uint8_t*, uint32_t) const override { return true; } + uint32_t capacity() const override { return 65536; } + uint32_t staged_size() const override { return size; } + void clear() override { size = 0; } +}; +class OtaStoreFlashNrf52 : public Store { +public: + OtaStoreFlashNrf52() { ++internal_live; assert(nor_live == 0); } + ~OtaStoreFlashNrf52() { --internal_live; } + bool plan_layout(bool full, uint32_t, uint32_t, uint32_t, bool bootloader) override { + return !full || bootloader; + } +}; +class OtaStoreQspiNrf52 : public Store { +public: + OtaStoreQspiNrf52() { ++nor_live; assert(internal_live == 0); } + ~OtaStoreQspiNrf52() { --nor_live; } + static uint8_t autoDetect() { assert(!card && !rak4631_ethernet_owns_spi()); ++probes; return detected; } + static bool headerW25Detected() { return false; } + uint32_t jedec_id() const { return 0xef4015; } + uint8_t status1() const { return 0; } + const char* last_stage() const { return "idle"; } + const char* last_error() const { return ""; } +}; +class OtaStoreSdNrf52 : public Store { +public: + bool probeMedia() { assert(card && !rak4631_ethernet_owns_spi()); ++sd_probes; return media; } + bool releaseBus() { return release_ok && size == 0; } + const char* last_error() const { return "SD test"; } +}; +#define ota_bootloader_app_caps appCaps +#define ota_bootloader_update_caps bootCaps +''' + body + r''' +} } +int main() { + { + OtaStoreAdaptiveNrf52 store; + auto* stable_sd = &store.sdStore(); + assert(!store.usesSd() && !store.usesQspi() && !store.usesInternal()); + assert(!store.usesExternal() && store.capacity() == 0 && store.staged_size() == 0); + assert(card_checks == 0 && sd_probes == 0 && probes == 0); + assert(store.selectStorage() && store.usesSd() && store.usesExternal()); + assert(sd_probes == 1 && probes == 0); + assert(store.plan_layout(true, 100000, 365, 100000, false)); + assert(store.begin(100370)); + assert(!store.resetSelection()); + // A malformed caller cannot grant a previously selected external store + // access after Ethernet took ownership, with or without the old combined + // recipe flag. Its staged data must remain untouched. + rak4631_set_ethernet_spi_owner(true); + uint8_t byte = 0; + assert(!store.begin(200) && !store.write(0, &byte, 1) && !store.read(0, &byte, 1)); + assert(store.capacity() == 0); + store.clear(); + assert(stable_sd->staged_size() == 100370); + rak4631_set_ethernet_spi_owner(false); + assert(!store.plan_layout(true, 40960, 365, 40960, true)); // never abandon SD data + assert(store.usesSd() && stable_sd->staged_size() == 100370); + store.clear(); + assert(store.plan_layout(true, 40960, 365, 40960, true)); + assert(store.usesInternal() && &store.sdStore() == stable_sd); + assert(store.begin(41330) && !store.resetSelection()); + store.clear(); + assert(store.plan_layout(true, 100000, 365, 100000, false) && store.usesSd()); + release_ok = false; + assert(!store.resetSelection() && store.usesSd()); + release_ok = true; + assert(store.resetSelection() && !store.usesSd()); + assert(&store.sdStore() == stable_sd); + } + assert(internal_live == 0 && nor_live == 0); + probes = sd_probes = card_checks = 0; card = false; + { + OtaStoreAdaptiveNrf52 store; + assert(store.selectStorage() && store.usesQspi() && !store.usesSd()); + assert(probes == 1 && sd_probes == 0); + assert(store.begin(100)); + assert(!store.resetSelection()); + store.clear(); release_ok = false; + assert(!store.resetSelection() && store.usesQspi() && nor_live == 1); + release_ok = true; assert(store.resetSelection() && nor_live == 0); + } + card = true; sd_cap = false; probes = sd_probes = 0; + { + OtaStoreAdaptiveNrf52 store; + assert(store.selectStorage() && store.usesInternal()); + assert(strstr(store.selectionReason(), "newer OTAFIX")); + assert(probes == 0 && sd_probes == 0); // don't clock SD or same-CS NOR + assert(!store.plan_layout(true, 100000, 365, 100000, false)); + } + sd_cap = true; media = false; + { + OtaStoreAdaptiveNrf52 store; + assert(store.selectStorage() && store.usesInternal()); + assert(probes == 0 && sd_probes == 1); + } + media = true; valid_identity = false; sd_probes = 0; + { + OtaStoreAdaptiveNrf52 store; + assert(!store.selectStorage()); + assert(!store.usesExternal() && !store.usesInternal()); + assert(sd_probes == 0 && probes == 0); + } + valid_identity = true; device_name = "3401_DFU"; + { + OtaStoreAdaptiveNrf52 store; + assert(!store.selectStorage() && sd_probes == 0 && probes == 0); + } + device_name = "4631_DFU"; card_checks = 0; + rak4631_set_ethernet_spi_owner(true); + { + OtaStoreAdaptiveNrf52 store; + assert(store.selectStorage() && store.usesInternal()); + assert(card_checks == 0 && sd_probes == 0 && probes == 0); + } + rak4631_set_ethernet_spi_owner(false); + assert(internal_live == 0 && nor_live == 0); +} +''' + for legacy_combined in (False, True): + with self.subTest(legacy_combined=legacy_combined): + defines = ["OTA_RAK15002_SD_STORE", "OTA_INTERNAL_BOOTLOADER_UPDATE", + "OTA_RAK_AUTO_STORE"] + if legacy_combined: + defines.append("RAK4631_COMBINED_ETHERNET") + run_cpp(source, defines) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_rak_sd_room_ethernet.py b/test/test_rak_sd_room_ethernet.py new file mode 100644 index 000000000..862101224 --- /dev/null +++ b/test/test_rak_sd_room_ethernet.py @@ -0,0 +1,211 @@ +"""Optional-SD room Ethernet must use the guarded, opt-in runtime.""" + +from pathlib import Path +import os +import shutil +import subprocess +import tempfile +import unittest + +from test_rak4631_ethernet_runtime import FIXTURE, SCENARIOS +from test_rak_sd_receiver import method, run_cpp +from test_serial_wifi_sessions import SANITIZER_FLAGS + + +ROOT = Path(__file__).resolve().parents[1] + + +class RakSdRoomEthernetTest(unittest.TestCase): + def test_actual_role_callbacks_reserve_only_after_storage_release(self): + source = (ROOT / "examples/simple_room_server/main.cpp").read_text() + callbacks = "\n".join(method(source, signature) for signature in ( + "static bool prepareSdEthernet(", + "static bool canReleaseSdEthernet(", + "static bool releaseSdEthernet(", + )) + harness = r''' +#include +#include +#include +#include "src/helpers/nrf52/Rak4631SharedSpi.h" +namespace mesh { namespace ota { +static bool fitted = false, prepare_ok = true, release_ok = true; +static unsigned prepares = 0, releases = 0; +struct Context { + bool prepareRakSdForEthernet() { + ++prepares; + assert(!rak4631_ethernet_owns_spi()); + return prepare_ok; + } + bool canReleaseRakSdForEthernet() { return release_ok; } + bool releasedRakSdEthernet() { + ++releases; + assert(rak4631_ethernet_owns_spi()); // Keep reservation until callback completes. + return release_ok; + } +}; +static Context context; +static Context* active = nullptr; +Context* ota_context_if_active() { return active; } +bool ota_rak15002_sd_card_present() { return fitted; } +} } +''' + callbacks + r''' +int main() { + using namespace mesh::ota; + char error[160]; + assert(!prepareSdEthernet(nullptr, error, sizeof(error))); + assert(strstr(error, "not ready") && prepares == 0); + active = &context; + fitted = true; + assert(!prepareSdEthernet(nullptr, error, sizeof(error))); + assert(strstr(error, "SD card fitted") && prepares == 0); + fitted = false; prepare_ok = false; + assert(!prepareSdEthernet(nullptr, error, sizeof(error))); + assert(rak4631_spi_owner() == RakSharedSpiOwner::None); + prepare_ok = true; + assert(rak4631_try_acquire_spi(RakSharedSpiOwner::Nor)); + assert(!prepareSdEthernet(nullptr, error, sizeof(error))); + assert(rak4631_spi_owner() == RakSharedSpiOwner::Nor); // Never steal uncertain NOR. + rak4631_release_spi(RakSharedSpiOwner::Nor); + assert(prepareSdEthernet(nullptr, error, sizeof(error))); + assert(rak4631_ethernet_owns_spi()); + release_ok = false; + assert(!canReleaseSdEthernet(nullptr)); + assert(!releaseSdEthernet(nullptr) && rak4631_ethernet_owns_spi()); + active = nullptr; + assert(!releaseSdEthernet(nullptr) && rak4631_ethernet_owns_spi()); + active = &context; release_ok = true; + assert(canReleaseSdEthernet(nullptr) && releaseSdEthernet(nullptr)); + assert(rak4631_spi_owner() == RakSharedSpiOwner::None && releases == 2); +} +''' + run_cpp(harness, ("OTA_RAK15002_SD_STORE",)) + repeater = (ROOT / "examples/simple_repeater/main.cpp").read_text() + combined = "\n".join(method(repeater, signature) for signature in ( + "static bool prepareCombinedEthernet(", + "static bool canReleaseCombinedEthernet(", + "static bool releaseCombinedEthernet(", + )) + for original, replacement in ( + ("prepareCombinedEthernet", "prepareSdEthernet"), + ("canReleaseCombinedEthernet", "canReleaseSdEthernet"), + ("releaseCombinedEthernet", "releaseSdEthernet"), + ): + combined = combined.replace(original, replacement) + # No legacy NOR hooks exist in this mock. A wrong branch selected + # under both flags fails compilation before the ownership assertions. + run_cpp(harness.replace(callbacks, combined), + ("OTA_RAK15002_SD_STORE", "RAK4631_COMBINED_ETHERNET")) + + def test_actual_runtime_without_legacy_combined_flag(self): + compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++") + if compiler is None: + self.skipTest("a host C++17 compiler is required") + with tempfile.TemporaryDirectory(prefix="rak-sd-room-ethernet-") as directory: + binary = Path(directory) / "runtime" + fixture = (FIXTURE / "test.cpp").read_text() + fixture = fixture.replace("#include ", + "static std::atomic mock_sd_fitted{false};\n" + "#include ") + fixture = fixture.replace(" hooks.context = &context;", + " hooks.context = &context;\n" + " hooks.storage_conflict = [](void*) { return mock_sd_fitted.load(); };") + cases = r''' + case 35: { // Card insertion gates main loop and retains owner until explicit OFF. + start_online(); accept_connection(); + EthernetSpiAccess expected = EthernetSpiAccess::Idle; + wait_until([&] { expected = EthernetSpiAccess::Idle; + return ethernet_spi_access.compare_exchange_strong(expected, EthernetSpiAccess::Client); }); + const int before = hardware_operations.load(); + mock_sd_fitted.store(true); + ethernet_loop_maintain(); + ethernet_spi_access.store(EthernetSpiAccess::Idle); + wait_until([] { return ethernet_worker_stopped.load(); }); + assert(ethernet_state.load() == EthernetCliState::Faulted); + assert(!spi_active.load() && bus_owner.load() && ethernet_get_enabled()); + assert(hardware_operations.load() == before + 1); // SPI.end only: no socket CLOSE/SEND. + ethernet_loop_maintain(); + assert(bus_owner.load() && context.releases == 0); + assert(command("eth on").find("off first") != std::string::npos); + context.off_allowed = false; + assert(command("eth off").find("locked") != std::string::npos); + assert(bus_owner.load()); + context.off_allowed = true; + finish_off(); + break; + } + case 36: { // An entered vendor DHCP call completes; no further vendor IO follows insertion. + Ethernet.begin_blocked.store(true); + assert(command("eth on").find("starting") != std::string::npos); + wait_until([] { return Ethernet.begin_count.load() == 1; }); + const int before = hardware_operations.load(); + mock_sd_fitted.store(true); + unblock(Ethernet.begin_blocked); + wait_until([] { return ethernet_worker_stopped.load(); }); + assert(hardware_operations.load() == before + 1); + assert(!spi_active.load() && bus_owner.load()); + finish_off(); + break; + } + case 37: { // Retained Stream admission detects insertion before any controller register access. + start_online(); accept_connection(); + EthernetSpiAccess expected = EthernetSpiAccess::Idle; + wait_until([&] { expected = EthernetSpiAccess::Idle; + return ethernet_spi_access.compare_exchange_strong(expected, EthernetSpiAccess::Client); }); + const int before = hardware_operations.load(); + mock_sd_fitted.store(true); + assert_busy_facade(); + ethernet_spi_access.store(EthernetSpiAccess::Idle); + wait_until([] { return ethernet_worker_stopped.load(); }); + assert(hardware_operations.load() == before + 1 && bus_owner.load()); + finish_off(); + break; + } + case 38: { // Card inserted after reservation but before worker scheduling: no hardware init. + defer_task_start.store(true); + assert(command("eth on").find("starting") != std::string::npos); + mock_sd_fitted.store(true); + unblock(defer_task_start); + wait_until([] { return ethernet_worker_stopped.load(); }); + assert(hardware_operations.load() == 0 && !spi_active.load() && bus_owner.load()); + finish_off(); + break; + } +''' + fixture = fixture.replace(" default: assert(false);", cases + " default: assert(false);") + fixture_path = Path(directory) / "fixture.cpp" + fixture_path.write_text(fixture, encoding="utf-8") + compiled = subprocess.run([ + compiler, "-std=c++17", "-pthread", "-Wall", "-Wextra", "-Werror", + *SANITIZER_FLAGS, "-DETHERNET_ENABLED=1", "-DOTA_RAK15002_SD_STORE=1", + f"-I{FIXTURE / 'mocks'}", f"-I{ROOT / 'src'}", + str(fixture_path), "-o", str(binary), + ], capture_output=True, text=True, timeout=60) + self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr) + descriptions = (*SCENARIOS, "online SD insertion", "SD insertion during DHCP", + "retained Stream SD insertion", "SD insertion before worker start") + for scenario, description in enumerate(descriptions): + with self.subTest(scenario=description): + checked = subprocess.run([str(binary), str(scenario)], + capture_output=True, text=True, timeout=10) + self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr) + self.assertIn(f"PASS: RAK4631 Ethernet runtime scenario {scenario}", checked.stdout) + + def test_room_wires_default_off_after_mesh_begin_and_keeps_renewal_stream(self): + source = (ROOT / "examples/simple_room_server/main.cpp").read_text() + setup = method(source, "void setup()") + configure = setup.index("ethernet_configure(hooks)") + self.assertLess(setup.index("the_mesh.begin(fs)"), configure) + self.assertLess(configure, setup.index("ethernet_start_task()")) + self.assertNotIn("ethernet_configure(hooks, true)", setup) + for assignment in ("hooks.prepare = prepareSdEthernet", + "hooks.can_release = canReleaseSdEthernet", + "hooks.released = releaseSdEthernet", + "hooks.storage_conflict ="): + self.assertIn(assignment, setup) + self.assertIn("#if defined(OTA_RAK15002_SD_STORE)\n" + " if (ethernet_take_session_reset() || !ethernet_get_enabled())", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/mota/gen_targets.py b/tools/mota/gen_targets.py index 6d04974dd..8f93b379d 100644 --- a/tools/mota/gen_targets.py +++ b/tools/mota/gen_targets.py @@ -26,6 +26,7 @@ ROOT = Path(__file__).resolve().parents[2] OUT = ROOT / "src" / "helpers" / "ota" / "OtaTargets.h" INTERNAL_BOOTLOADER_TARGETS = ROOT / "tools" / "mota" / "nrf52_internal_bootloader_targets.txt" NAME_DECODE_CHUNK_SIZE = 512 +RAK_FULL_NAME_DECODE_CHUNK_SIZE = 4096 def resolved_config(): @@ -126,9 +127,22 @@ def _compressed_name_chunks(rows, max_chunk_size): def generate_header(rows): - data, offsets, raw_lengths, firsts = compressed_name_chunks(rows) capacity = max((len(name.encode("utf-8")) + 1 for _, name in rows), default=1) + def chunk_tables(chunk_size): + data, offsets, raw_lengths, firsts = compressed_name_chunks(rows, chunk_size) + lines = [f" enum {{ DECODE_CHUNK_BYTES = {chunk_size} }};"] + for name, values in (("OFFSETS", offsets), ("RAW_LENGTHS", raw_lengths), ("FIRSTS", firsts)): + lines += [f" static const uint16_t {name}[] = {{"] + lines += [" " + ", ".join(str(value) for value in values[i:i + 12]) + "," + for i in range(0, len(values), 12)] + lines += [" };"] + lines += [f" // {len(data)} encoded bytes; {len(raw_lengths)} independent chunks, at most {chunk_size} decoded bytes.", + " static const uint8_t NAMES[] = {"] + lines += [" " + ", ".join(f"0x{byte:02x}" for byte in data[i:i + 16]) + "," + for i in range(0, len(data), 16)] + return lines + [" };"] + out = [ "#pragma once", "#include ", @@ -151,6 +165,9 @@ def generate_header(rows): "", "#if OTA_TARGET_NAME_TABLE && OTA_TARGET_NAME_FRONT_CODED", ' #include "tinf/tinf.h"', + " #if defined(OTA_RAK15002_SD_STORE) && defined(COMPANION_RADIO_FULL)", + " #include ", + " #endif", "#endif", "", "namespace mesh { namespace ota {", @@ -167,33 +184,23 @@ def generate_header(rows): ] out += [" " + ", ".join(f"0x{tid:08x}" for tid, _ in rows[i:i + 8]) + "," for i in range(0, len(rows), 8)] + out += [" };", "#if defined(OTA_RAK15002_SD_STORE) && defined(COMPANION_RADIO_FULL)"] + out += chunk_tables(RAK_FULL_NAME_DECODE_CHUNK_SIZE) + out += ["#else"] + chunk_tables(NAME_DECODE_CHUNK_SIZE) + ["#endif"] out += [ - " };", - " static const uint16_t OFFSETS[] = {", - ] - out += [" " + ", ".join(str(offset) for offset in offsets[i:i + 12]) + "," - for i in range(0, len(offsets), 12)] - out += [" };", " static const uint16_t RAW_LENGTHS[] = {"] - out += [" " + ", ".join(str(length) for length in raw_lengths[i:i + 12]) + "," - for i in range(0, len(raw_lengths), 12)] - out += [" };", " static const uint16_t FIRSTS[] = {"] - out += [" " + ", ".join(str(first) for first in firsts[i:i + 12]) + "," - for i in range(0, len(firsts), 12)] - out += [ - " };", - f" // {len(data)} encoded bytes; {len(raw_lengths)} independent chunks, at most {NAME_DECODE_CHUNK_SIZE} decoded bytes.", - " static const uint8_t NAMES[] = {", - ] - out += [" " + ", ".join(f"0x{byte:02x}" for byte in data[i:i + 16]) + "," - for i in range(0, len(data), 16)] - out += [ - " };", " for (unsigned i = 0; i < sizeof(IDS) / sizeof(IDS[0]); i++) {", " if (IDS[i] != target_id) continue;", " unsigned group = 0;", " while (i >= FIRSTS[group + 1]) group++;", - f" uint8_t raw[{NAME_DECODE_CHUNK_SIZE}];", - " unsigned int produced = sizeof raw;", + "#if defined(OTA_RAK15002_SD_STORE) && defined(COMPANION_RADIO_FULL)", + " // Rare display lookup: larger chunks save flash without growing the loop stack.", + " uint8_t* raw = (uint8_t*)malloc(DECODE_CHUNK_BYTES);", + " if (!raw) return nullptr;", + " struct Scratch { uint8_t* bytes; ~Scratch() { free(bytes); } } scratch = {raw};", + "#else", + " uint8_t raw[DECODE_CHUNK_BYTES];", + "#endif", + " unsigned int produced = DECODE_CHUNK_BYTES;", " if (tinf_uncompress_exact(raw, &produced, NAMES + OFFSETS[group],", " OFFSETS[group + 1] - OFFSETS[group]) != TINF_OK ||", " produced != RAW_LENGTHS[group]) return nullptr;", diff --git a/tools/mota/nrf52_internal_bootloader_targets.txt b/tools/mota/nrf52_internal_bootloader_targets.txt index 0fc554489..9d98f5226 100644 --- a/tools/mota/nrf52_internal_bootloader_targets.txt +++ b/tools/mota/nrf52_internal_bootloader_targets.txt @@ -3,7 +3,10 @@ # shared allowlist for build.sh release aliases and the subset that are also # literal PlatformIO envs. Synthetic aliases are activated by build.sh. # External QSPI/SD, internal ExtraFS, seeder-only, Ethernet, and full-sensor -# roles are intentionally absent. +# roles are intentionally absent, except for the explicitly listed RAK4631 +# adaptive receiver aliases below. Their optional RAK15002 SD path retains +# the existing internal/NOR shared-slot contract and must use the matching +# all-in-one OTAFIX loader; it is not a generic SD-primary profile. Heltec_tower_v2_repeater_lora_ota_no_external_sensors Heltec_t096_repeater_lora_ota_no_external_sensors Heltec_t096_repeater_bridge_rs232_lora_ota_no_external_sensors @@ -20,6 +23,9 @@ ThinkNode_M3_repeater_lora_ota_no_external_sensors RAK_3401_repeater_lora_ota_no_external_sensors RAK_4631_repeater_lora_ota_no_external_sensors RAK_4631_repeater_unified_lora_ota +RAK_4631_room_server_lora_ota_no_external_sensors +RAK_4631_room_server_ethernet_lora_ota_no_external_sensors +RAK_4631_sensor_lora_ota_no_external_sensors RAK_3401_repeater_unified_lora_ota RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors diff --git a/tools/mota/pio_endf.py b/tools/mota/pio_endf.py index c52d8f911..8934ebca7 100644 --- a/tools/mota/pio_endf.py +++ b/tools/mota/pio_endf.py @@ -268,9 +268,18 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f linked_app_end = app_start + app_region_size internal_extrafs = (_cppdef("EXTRAFS") is not None and _cppdef("QSPIFLASH") is None and _builds_companion_radio()) - sd_backed = _cppdef("OTA_SD_STORE") is not None + sd_enabled = _cppdef("OTA_SD_STORE") is not None qspi_backed = _cppdef("OTA_QSPI_STORE") is not None auto_store = _cppdef("OTA_RAK_AUTO_STORE") is not None + seeder_only = _cppdef("OTA_SEEDER_ONLY") is not None + optional_rak_sd = _cppdef("OTA_RAK15002_SD_STORE") is not None + if optional_rak_sd and (not sd_enabled or _cppdef("RAK_4631") is None or + not (auto_store or (seeder_only and _builds_companion_radio()))): + raise RuntimeError("optional RAK15002 SD requires an exact RAK4631 adaptive receiver or source-only Companion") + # Optional SD does not change the frozen application layout: receivers + # retain AUTO+HYBRID internal staging, and Full Companion retains ExtraFS. + # The runtime selects SD only after validating its separate loader ABI. + sd_backed = sd_enabled and not optional_rak_sd qspi_bootloader_update = _cppdef("OTA_QSPI_BOOTLOADER_UPDATE") is not None hybrid_ram = _cppdef("OTA_HYBRID_RAM_STORE") is not None internal_bootloader_update = _cppdef("OTA_INTERNAL_BOOTLOADER_UPDATE") is not None @@ -315,7 +324,6 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f body = ml.ensure_nrf52_layout(endf_free_body, layout) ident = _firmware_ident() out, h8 = ml.ensure_endf(body, ident) - seeder_only = _cppdef("OTA_SEEDER_ONLY") is not None if seeder_only: # A source-only full Companion never stages or applies an update to # itself, so it does not need to fit in OTAFIX's in-place workspace. diff --git a/tools/mota/test_mota.py b/tools/mota/test_mota.py index 0cf55f79f..ea20f54f4 100644 --- a/tools/mota/test_mota.py +++ b/tools/mota/test_mota.py @@ -52,7 +52,16 @@ def test_internal_bootloader_target_wiring_is_central_and_not_duplicated(): if line.strip() and not line.lstrip().startswith("#") ] assert len(inventory) == len(set(inventory)) - assert len(inventory) == 23 + # The RAK4631 adaptive RAK15002 receiver contract deliberately adds the + # three generated receiver roles to the established exact inventory. Keep + # the count and the names fixed so an unrelated target cannot silently + # acquire the retained-RAM / internal-bootloader-update layout. + assert len(inventory) == 26 + assert { + "RAK_4631_room_server_lora_ota_no_external_sensors", + "RAK_4631_room_server_ethernet_lora_ota_no_external_sensors", + "RAK_4631_sensor_lora_ota_no_external_sensors", + } <= set(inventory) gat_targets = { "GAT562_30S_Mesh_Kit_repeater_lora_ota_no_external_sensors", "GAT562_Mesh_Tracker_Pro_repeater_lora_ota_no_external_sensors", @@ -1019,13 +1028,25 @@ def test_ota_target_generation_honors_explicit_disable(): def test_rak_nrf52_ota_profiles_keep_ina_and_gps_where_uart_is_available(): root = Path(__file__).resolve().parents[2] - def env_section(path, name): + def section(path, header): text = path.read_text(encoding="utf-8") - return text.split(f"[env:{name}]", 1)[1].split("\n[", 1)[0] + return text.split(f"[{header}]", 1)[1].split("\n[", 1)[0] + + def env_section(path, name): + return section(path, f"env:{name}") rak4631_path = root / "variants/rak4631/platformio.ini" + rak4631_reduced = section(rak4631_path, "rak4631_repeater_reduced_ota_base") + rak4631_reduced_wrapper = env_section( + rak4631_path, "RAK_4631_repeater_lora_ota_no_external_sensors" + ) + # The adaptive storage flags live in the wrapper; the pre-adaptive base + # remains the single source of the reduced INA/GPS contract and is also + # deliberately reused by the strict historical SD cache seeder. + assert "${rak4631_repeater_reduced_ota_base.build_flags}" in rak4631_reduced_wrapper + assert "${rak4631_repeater_reduced_ota_base.lib_deps}" in rak4631_reduced_wrapper gps_profiles = ( - env_section(rak4631_path, "RAK_4631_repeater_lora_ota_no_external_sensors"), + rak4631_reduced, env_section( rak4631_path, "RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors", diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 8f519d5ea..9d70f32b0 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -122,13 +122,14 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> -; One full-sensor repeater image for internal flash, W25Q16, or RAK15001 C. -; The matching OTAFIX bootloader for the installed hardware is still required. +; One full-sensor repeater image selects internal flash, qualified NOR, or an +; inserted RAK15002 card. The matching OTAFIX bootloader for the selected +; internal/NOR handoff is still required. [env:RAK_4631_repeater_unified_lora_ota] extends = rak4631 build_flags = ${rak4631.build_flags} - ${nrf52_wisblock_auto_ota.build_flags} + ${nrf52_rak4631_unified_ota.build_flags} -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME='"RAK4631 Repeater"' -D ADVERT_LAT=0.0 @@ -149,16 +150,14 @@ build_src_filter = ${rak4631.build_src_filter} + + +<../examples/simple_repeater> +lib_deps = + ${rak4631.lib_deps} + ${nrf52_rak4631_unified_ota.lib_deps} -; Lean OTA target for space-constrained RAK4631 repeaters. This removes optional external -; environmental sensor packages while retaining the common INA I2C voltage/current monitors and -; RAK12500/RAK12501 GPS support. The legacy no_external_sensors target name remains stable for OTA -; compatibility. RAK4631Board::getBattMilliVolts(), repeater telemetry, and battery.alert remain part -; of the board/repeater sources below. -; RAK12500 and the configured INA3221 both use I2C address 0x42. When both are required, keep GPS at -; 0x42, strap INA3221 A0 to SCL for 0x43, and build with -DTELEM_INA3221_ADDRESS=0x43. -[env:RAK_4631_repeater_lora_ota_no_external_sensors] -extends = rak4631 +; Shared reduced RAK4631 repeater recipe. The old cache-only SD seeder uses +; this pre-adaptive base so its strict source-only contract cannot inherit the +; receiver's internal/NOR/SD STORE flags. +[rak4631_repeater_reduced_ota_base] extra_scripts = ${nrf52_lora_ota.extra_scripts} build_flags = ${rak4631.build_flags} @@ -192,14 +191,39 @@ lib_deps = adafruit/Adafruit SSD1306 @ ^2.5.13 sparkfun/SparkFun u-blox GNSS Arduino Library@^2.2.27 +; Lean OTA target for space-constrained RAK4631 repeaters. This removes optional external +; environmental sensor packages while retaining the common INA I2C voltage/current monitors and +; RAK12500/RAK12501 GPS support. The legacy no_external_sensors target name remains stable for OTA +; compatibility. RAK4631Board::getBattMilliVolts(), repeater telemetry, and battery.alert remain part +; of the board/repeater sources below. +; RAK12500 and the configured INA3221 both use I2C address 0x42. When both are required, keep GPS at +; 0x42, strap INA3221 A0 to SCL for 0x43, and build with -DTELEM_INA3221_ADDRESS=0x43. +[env:RAK_4631_repeater_lora_ota_no_external_sensors] +extends = rak4631 +extra_scripts = ${rak4631_repeater_reduced_ota_base.extra_scripts} +build_flags = + ${rak4631_repeater_reduced_ota_base.build_flags} + ${nrf52_rak4631_unified_ota.build_flags} +build_src_filter = ${rak4631_repeater_reduced_ota_base.build_src_filter} +lib_deps = + ${rak4631_repeater_reduced_ota_base.lib_deps} + ${nrf52_rak4631_unified_ota.lib_deps} + ; Experimental exact RAK15002 IO-slot SD archive. This is source-only: it ; caches/serves peer mOTAs but never installs an application or bootloader. ; Keep the ordinary reduced sensor/GPS/OLED/BLE features; do not substitute it ; for a receiver in the automatic Full/Reduced OTA release matrix. [env:RAK_4631_repeater_rak15002_sd_seeder_only] extends = env:RAK_4631_repeater_lora_ota_no_external_sensors +; Its parent now carries the adaptive receiver recipe. Keep this historical +; cache-only archive exactly source-only and mutually exclusive with it. +build_unflags = + ${nrf52_base.build_unflags} + -D OTA_RAK_AUTO_STORE=1 + -D OTA_QSPI_STORE=1 + -D OTA_RAK15002_SD_STORE=1 build_flags = - ${env:RAK_4631_repeater_lora_ota_no_external_sensors.build_flags} + ${rak4631_repeater_reduced_ota_base.build_flags} -UOTA_FLASH_STORE -D OTA_SEEDER_ONLY=1 -D OTA_SD_STORE=1 @@ -212,7 +236,7 @@ build_flags = -UADVERT_NAME -D ADVERT_NAME='"RAK4631 SD Seeder"' lib_deps = - ${env:RAK_4631_repeater_lora_ota_no_external_sensors.lib_deps} + ${rak4631_repeater_reduced_ota_base.lib_deps} https://github.com/greiman/SdFat-beta.git#a249cfa121e25a96ee14d8813abf01755f1f6a25 [env:RAK_4631_repeater_ethernet] @@ -358,6 +382,9 @@ build_flags = build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${rak4631.lib_deps} + ${nrf52_rak15002_sd_store.lib_deps} [env:RAK_4631_room_server_ethernet] extends = rak4631 @@ -378,6 +405,7 @@ build_src_filter = ${rak4631.build_src_filter} +<../examples/simple_room_server> lib_deps = ${rak4631.lib_deps} + ${nrf52_rak15002_sd_store.lib_deps} https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip [env:RAK_4631_companion_radio_usb] @@ -440,6 +468,9 @@ lib_deps = extends = env:RAK_4631_companion_radio_usb build_flags = ${env:RAK_4631_companion_radio_usb.build_flags} + ; Optional RAK15002 source/cache. Runtime arbitration keeps the shared + ; WisBlock IO slot safe when RAK13800 Ethernet is also compiled. + ${nrf52_rak15002_sd_store.build_flags} -D ETHERNET_ENABLED=1 -D ETHERNET_USE_RAK13800 -D ETHERNET_CLASS=RAK13800EthernetInterface @@ -449,6 +480,7 @@ build_src_filter = ${env:RAK_4631_companion_radio_usb.build_src_filter} + lib_deps = ${env:RAK_4631_companion_radio_usb.lib_deps} + ${nrf52_rak15002_sd_store.lib_deps} https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip @@ -513,6 +545,9 @@ build_flags = build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_sensor> +lib_deps = + ${rak4631.lib_deps} + ${nrf52_rak15002_sd_store.lib_deps} [env:RAK_4631_kiss_modem] extends = rak4631