Add adaptive RAK4631 SD storage to full firmware profiles

This commit is contained in:
mikecarper committed 2026-10-16 01:18:26 -07:00
1 parent 9181e3e0ec
commit 774c2adc80
54 files changed
+3962 -477

No files matched your search

+5
View File
@@ -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
+83 -9
View File
@@ -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"
+47 -3
View File
@@ -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.
+32 -4
View File
@@ -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,
+121
View File
@@ -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 <MID8> 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.
+10
View File
@@ -53,6 +53,9 @@
#endif
#if defined(ENABLE_OTA)
#include <helpers/ota/OtaContext.h>
#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 <helpers/ota/OtaSpeedConfig.h>
@@ -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 <MHz>,<kHz>,<SF>,<CR>[,<preamble>]\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
+51
View File
@@ -653,6 +653,53 @@ static void cancelCompanionEthernetSession(void*) {
the_mesh.cancelSerialOperationsForRoute(&ethernet_interface);
interface_manager.forgetReplyRouteForDisconnected(&ethernet_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, &ethernet_interface);
#endif
+26
View File
@@ -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();
+48
View File
@@ -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 <helpers/ota/OtaContext.h>
// 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;
}
+28
View File
@@ -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
+15 -3
View File
@@ -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"])
+20 -1
View File
@@ -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()) {
+7
View File
@@ -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.
@@ -4,6 +4,9 @@
#include "../../nrf52/EthernetCliTransmit.h"
#include <SPI.h>
#include <EthernetUdp.h>
#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()) {
@@ -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;
+23 -10
View File
@@ -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<bool> ethernet_running{false};
#if defined(RAK4631_COMBINED_ETHERNET)
#if MESH_RAK_ETHERNET_CLI_RUNTIME
static std::atomic<bool> 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];
+60 -8
View File
@@ -3,13 +3,17 @@
#include <cstdlib>
#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<bool> ethernet_command_check_requested{false};
static std::atomic<uint32_t> ethernet_command_check_started{0};
static bool ethernet_controller_faulted = false;
#if defined(OTA_RAK15002_SD_STORE)
static std::atomic<bool> 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<uint32_t>(ip[0]) |
(static_cast<uint32_t>(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<uint32_t>(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);
+30 -5
View File
@@ -1,8 +1,9 @@
#pragma once
#if defined(RAK4631_COMBINED_ETHERNET)
#if defined(RAK4631_COMBINED_ETHERNET) || defined(OTA_RAK15002_SD_STORE)
#include <atomic>
#include <stdint.h>
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<bool>& rak4631_ethernet_spi_owner() {
static std::atomic<bool> owner{false};
enum class RakSharedSpiOwner : uint8_t { None, Ethernet, Sd, Nor };
inline std::atomic<RakSharedSpiOwner>& rak4631_shared_spi_owner() {
static std::atomic<RakSharedSpiOwner> 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
+37 -8
View File
@@ -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,
+4
View File
@@ -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)
+40 -2
View File
@@ -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(
+2 -1
View File
@@ -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
+33 -10
View File
@@ -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;
+38 -5
View File
@@ -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/<mid>.mota; an in-progress capture uses /mota/<mid>.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;
+73 -10
View File
@@ -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 <id> flash | install | bootloader | cancel | announce | self | serve | storage | folder | config | key");
"OTA: status | stats | ls | get <id> 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();
+108 -2
View File
@@ -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());
+27 -2
View File
@@ -2,6 +2,17 @@
#include <SPI.h>
#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);
+30
View File
@@ -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
+104 -19
View File
@@ -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 <new>
@@ -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<OtaStore*>(&_storage.external) :
_mode == INTERNAL_MODE ? static_cast<OtaStore*>(&_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<const OtaStore*>(&_storage.external) :
_mode == INTERNAL_MODE ? static_cast<const OtaStore*>(&_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); }
+33 -3
View File
@@ -13,9 +13,12 @@
#include <SPI.h>
#endif
#include <string.h>
#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) {
+5 -2
View File
@@ -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
+131 -18
View File
@@ -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<volatile uint8_t*>(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
+13 -5
View File
@@ -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;
+532 -287
View File
@@ -3,7 +3,7 @@
#include <stddef.h>
// 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 <stdlib.h>
#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" },
@@ -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
+11
View File
@@ -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) {}
};
+9 -1
View File
@@ -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<uint8_t> loaded(length);
assert(cache.read(0, 0, loaded.data(), length));
assert(!memcmp(loaded.data(), bytes, length));
+2
View File
@@ -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()",
+42
View File
@@ -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 (
+41
View File
@@ -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 = "+<examples/companion_radio>"
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:
+8
View File
@@ -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()
+17 -3
View File
@@ -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 <stdlib.h>\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):
+29 -3
View File
@@ -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"]):
+458
View File
@@ -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 <cassert>
#include <cstdint>
#include <vector>
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<int> 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 <helpers/ota/OtaSdSpiBusNrf52.h>
#include <cassert>
#include <cstring>
#include <new>
#include <initializer_list>
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<RakSharedSpiOwner>& rak4631_shared_spi_owner()",
"static std::atomic<RakSharedSpiOwner>& rak4631_shared_spi_owner()")
self.assertNotEqual(header, broken)
local_app = app.replace("<helpers/ota/OtaSdSpiBusNrf52.h>", '"Owner.h"')
local_other = other.replace("<helpers/ota/OtaSdSpiBusNrf52.h>", '"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 <helpers/ota/OtaSdTypesNrf52.h>
class SdFat32 {};
class FatFile {};
#include <type_traits>
static_assert(std::is_same<mesh::ota::OtaSdFs, SdFat32>::value, "RAK store must be FAT16/FAT32 only");
static_assert(std::is_same<mesh::ota::OtaSdFile, FatFile>::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 <helpers/ota/OtaSdTypesNrf52.h>
class SdFs {};
class FsFile {};
#include <type_traits>
static_assert(std::is_same<mesh::ota::OtaSdFs, SdFs>::value, "legacy SD must retain SdFs");
static_assert(std::is_same<mesh::ota::OtaSdFile, FsFile>::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 <helpers/UserGpioPinPolicy.h>
#include <cassert>
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 <helpers/UserGpioPinPolicy.h>
#include <cassert>
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 <helpers/ota/OtaStore.h>\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 <helpers/ota/OtaStore.h>
#include <cassert>
#include <cstring>
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 <Arduino.h>", "#include <Arduino.h>\n#include <helpers/ota/OtaSdSpiBusNrf52.h>")
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()
+341
View File
@@ -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", ["+<helpers/ota/*.cpp>"]],
]],
["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", ["+<helpers/ota/*.cpp>"]],
]],
]
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()
+1 -1
View File
@@ -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 <cassert>
+183
View File
@@ -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 <cassert>
#include <cstdlib>
#include <cstdint>
#include <initializer_list>
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<uint8_t*>(malloc(8));
c.serve_self_proof = static_cast<uint8_t*>(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 <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
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()
+535
View File
@@ -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 <cassert>
#include <cstring>
#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<class Store> 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 <cassert>
#include <cstdio>
#include <cstring>
#include <cstdint>
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 <cassert>
#include <cstdarg>
#include <cstdio>
#include <cstring>
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 <cassert>
#include <cstdarg>
#include <cstdio>
#include <cstring>
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 <cassert>
#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 <cassert>
#include <cstring>
#include <sys/mman.h>
#include "src/helpers/ota/OtaBlInfo.h"
using namespace mesh::ota;
int main() {
auto* flash = static_cast<uint8_t*>(mmap(reinterpret_cast<void*>(uintptr_t(0xf4000)),
0xa000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
assert(flash == reinterpret_cast<uint8_t*>(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 <cassert>
#include <cstring>
#include <initializer_list>
#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 <cassert>
#include <cstring>
#include <new>
#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()
+211
View File
@@ -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 <cassert>
#include <cstdio>
#include <cstring>
#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 <helpers/nrf52/EthernetCLI.h>",
"static std::atomic<bool> mock_sd_fitted{false};\n"
"#include <helpers/nrf52/EthernetCLI.h>")
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()
+30 -23
View File
@@ -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 <stdint.h>",
@@ -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 <stdlib.h>",
" #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;",
@@ -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
+10 -2
View File
@@ -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.
+25 -4
View File
@@ -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",
+49 -14
View File
@@ -122,13 +122,14 @@ build_src_filter = ${rak4631.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/bridges/RS232Bridge.cpp>
+<../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}
+<helpers/ui/SSD1306Display.cpp>
+<../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}
+<helpers/ethernet/RAK13800/>
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}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_sensor>
lib_deps =
${rak4631.lib_deps}
${nrf52_rak15002_sd_store.lib_deps}
[env:RAK_4631_kiss_modem]
extends = rak4631