Add adaptive RAK4631 SD storage to full firmware profiles
This commit is contained in:
1 parent
9181e3e0ec
commit
774c2adc80
54 files changed
+3962
-477
No files matched your search
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -653,6 +653,53 @@ static void cancelCompanionEthernetSession(void*) {
|
||||
the_mesh.cancelSerialOperationsForRoute(ðernet_interface);
|
||||
interface_manager.forgetReplyRouteForDisconnected(ðernet_interface);
|
||||
}
|
||||
#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800)
|
||||
static bool prepareCompanionRakEthernet(void*) {
|
||||
auto* context = mesh::ota::ota_context_if_active();
|
||||
return !context || context->prepareRakSdForEthernet();
|
||||
}
|
||||
static bool canReleaseCompanionRakEthernet(void*) {
|
||||
auto* context = mesh::ota::ota_context_if_active();
|
||||
return !context || context->canReleaseRakSdForEthernet();
|
||||
}
|
||||
static bool releasedCompanionRakEthernet(void*) {
|
||||
auto* context = mesh::ota::ota_context_if_active();
|
||||
return !context || context->releasedRakSdEthernet();
|
||||
}
|
||||
|
||||
bool handleRak4631PeripheralCommand(const char* command, char* reply, size_t capacity) {
|
||||
const bool get = strcmp(command, "get eth") == 0;
|
||||
const char* value = strncmp(command, "set eth ", 8) == 0 ? command + 8 : nullptr;
|
||||
if (get || value) {
|
||||
if (value && strcmp(value, "on") && strcmp(value, "off")) {
|
||||
snprintf(reply, capacity, "Error: use set eth on|off");
|
||||
} else if (value && !(strcmp(value, "on") == 0
|
||||
? ethernet_interface.tryEnable() : ethernet_interface.tryDisable())) {
|
||||
snprintf(reply, capacity, "Error: %s", ethernet_interface.last_error());
|
||||
} else {
|
||||
snprintf(reply, capacity, "%s eth=%s; RAK13800 (this boot)", get ? ">" : "OK -",
|
||||
ethernet_interface.hardwareActive() ? "on" : "off");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (strcmp(command, "get sdcard") == 0 || strcmp(command, "get sdcard free") == 0) {
|
||||
if (ethernet_interface.hardwareActive()) {
|
||||
snprintf(reply, capacity, "Error: turn Ethernet off before accessing SD");
|
||||
} else if (mesh::ota::ota_acquire_context(reply, capacity)) {
|
||||
uint64_t used = 0, free = 0;
|
||||
auto& card = mesh::ota::ota_ctx().sdStagingStore();
|
||||
if (card.getSpace(board, used, free)) {
|
||||
snprintf(reply, capacity, "> SD used=%llu free=%llu bytes; ota cache for archive status",
|
||||
(unsigned long long)used, (unsigned long long)free);
|
||||
} else {
|
||||
snprintf(reply, capacity, "Error: %s", card.last_error());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
@@ -3950,6 +3997,10 @@ void setup() {
|
||||
// add ethernet interface
|
||||
#if defined(ETHERNET_ENABLED)
|
||||
ethernet_interface.setSessionChangedCallback(cancelCompanionEthernetSession, nullptr);
|
||||
#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800)
|
||||
ethernet_interface.setSpiOwnershipHooks(prepareCompanionRakEthernet,
|
||||
canReleaseCompanionRakEthernet, releasedCompanionRakEthernet, nullptr);
|
||||
#endif
|
||||
ethernet_interface.begin();
|
||||
interface_manager.addInterface(InterfaceType::Ethernet, ðernet_interface);
|
||||
#endif
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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); }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {}
|
||||
};
|
||||
Vendored
+9
-1
@@ -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));
|
||||
|
||||
@@ -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()",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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"]):
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user