mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-01 22:48:17 +00:00
Add external QSPI staging for nRF52 OTA
This commit is contained in:
@@ -21,14 +21,21 @@ runs:
|
||||
run: |
|
||||
pip install --upgrade platformio
|
||||
|
||||
# a git tag of "room-server-v1.2.3" should set "v1.2.3" as GIT_TAG_VERSION
|
||||
# Keep the complete version suffix. For example:
|
||||
# - room-server-v1.2.3 -> v1.2.3
|
||||
# - repeater-room-v1.2.3-channel-dev-abc12345 -> v1.2.3-channel-dev-abc12345
|
||||
- name: Extract Version from Git Tag
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
||||
# triggered by a tag push (e.g: refs/tags/companion-v1.2.3)
|
||||
GIT_TAG_NAME="${GITHUB_REF#refs/tags/}"
|
||||
VERSION_STRING="${GIT_TAG_NAME##*-}"
|
||||
if [[ "${GIT_TAG_NAME}" =~ (^|-)(v[0-9][-0-9A-Za-z._+]*)$ ]]; then
|
||||
VERSION_STRING="${BASH_REMATCH[2]}"
|
||||
else
|
||||
echo "::error::Cannot extract a v-prefixed version from tag '${GIT_TAG_NAME}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# triggered by a workflow dispatch (e.g: refs/heads/main)
|
||||
# strip "refs/heads/" prefix and replace any remaining "/" with "-" to protect file paths
|
||||
|
||||
@@ -7,6 +7,7 @@ declare -A PIO_ENV_BOARD_BY_NAME=()
|
||||
declare -A PIO_ENV_MQTT_BY_NAME=()
|
||||
declare -A PIO_ENV_OTA_BY_NAME=()
|
||||
declare -A PIO_ENV_SD_OTA_BY_NAME=()
|
||||
declare -A PIO_ENV_QSPI_OTA_BY_NAME=()
|
||||
declare -A PIO_ENV_BUILD_BASE_BY_NAME=()
|
||||
declare -A PIO_ENV_FULL_BUILD_BY_NAME=()
|
||||
declare -A PIO_ENV_FULL_WIFI_OTA_BY_NAME=()
|
||||
@@ -43,7 +44,7 @@ PARSED_COMMAND_ARGS=()
|
||||
FIRMWARE_VERSION_EXPLICIT=0
|
||||
OUTPUT_POLICY_EXPLICIT=0
|
||||
|
||||
ENV_VARIANT_SUFFIX_PATTERN='companion_radio_(wifi_mqtt|serial|wifi|usb|ble|full)(_ps)?(_fem(on|off))?|companion_radio_ethernet|comp_radio_usb|companion_usb|companion_ble|repeater_bridge_rs232_serial1_lora_ota_no_external_sensors|repeater_bridge_rs232_serial2_lora_ota_no_external_sensors|repeater_bridge_rs232_lora_ota_no_external_sensors|repeater_lora_ota_no_external_sensors|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|repeater_observer_mqtt|repeater_ethernet|room_server_observer_mqtt|room_server_ethernet|terminal_chat|room_server|room_svr|kiss_modem|sensor|repeatr|repeater'
|
||||
ENV_VARIANT_SUFFIX_PATTERN='companion_radio_(wifi_mqtt|serial|wifi|usb|ble|full)(_ps)?(_fem(on|off))?|companion_radio_ethernet|comp_radio_usb|companion_usb|companion_ble|repeater_bridge_rs232_serial1_lora_ota_no_external_sensors|repeater_bridge_rs232_serial2_lora_ota_no_external_sensors|repeater_bridge_rs232_lora_ota_no_external_sensors|repeater_rak15001_slot_c_lora_ota|repeater_lora_ota_no_external_sensors|repeater_bridge_rs232_serial1|repeater_bridge_rs232_serial2|repeater_bridge_rs232|repeater_bridge_espnow|repeater_observer_mqtt|repeater_ethernet|room_server_observer_mqtt|room_server_ethernet|terminal_chat|room_server|room_svr|kiss_modem|sensor|repeatr|repeater'
|
||||
BOARD_MODIFIER_WITHOUT_DISPLAY="_without_display"
|
||||
BOARD_MODIFIER_LOGGING="_logging"
|
||||
BOARD_MODIFIER_TFT="_tft"
|
||||
@@ -95,7 +96,7 @@ Commands:
|
||||
build-sensor-firmwares: Build all sensor firmwares for all build targets.
|
||||
build-kiss-radio-firmwares: Build all KISS radio firmwares for all build targets.
|
||||
get-companion-firmwares-to-build: List USB and BLE companion targets for release automation.
|
||||
get-repeater-firmwares-to-build: List standard repeater targets for release automation.
|
||||
get-repeater-firmwares-to-build: List standard and specialized external-storage repeater targets for release automation.
|
||||
get-room-server-firmwares-to-build: List standard room-server targets for release automation.
|
||||
|
||||
Options:
|
||||
@@ -189,7 +190,7 @@ init_project_context() {
|
||||
fi
|
||||
|
||||
if [ ${#SUPPORTED_PIO_ENVS[@]} -eq 0 ]; then
|
||||
while IFS=$'\t' read -r env_name env_platform env_mqtt env_ota env_sd_ota env_full env_full_wifi env_board; do
|
||||
while IFS=$'\t' read -r env_name env_platform env_mqtt env_ota env_sd_ota env_qspi_ota env_full env_full_wifi env_board; do
|
||||
if [ -z "$env_name" ] || [ -z "$env_platform" ]; then
|
||||
continue
|
||||
fi
|
||||
@@ -199,6 +200,7 @@ init_project_context() {
|
||||
PIO_ENV_MQTT_BY_NAME["$env_name"]=$env_mqtt
|
||||
PIO_ENV_OTA_BY_NAME["$env_name"]=$env_ota
|
||||
PIO_ENV_SD_OTA_BY_NAME["$env_name"]=$env_sd_ota
|
||||
PIO_ENV_QSPI_OTA_BY_NAME["$env_name"]=$env_qspi_ota
|
||||
PIO_ENV_FULL_BUILD_BY_NAME["$env_name"]=$env_full
|
||||
PIO_ENV_FULL_WIFI_OTA_BY_NAME["$env_name"]=$env_full_wifi
|
||||
done < <(
|
||||
@@ -217,6 +219,7 @@ for section, options in data:
|
||||
ota_enabled = False
|
||||
ota_disabled = False
|
||||
sd_ota = False
|
||||
qspi_ota = False
|
||||
admin_enabled = False
|
||||
espnow_enabled = "bridge_espnow" in env_name.lower()
|
||||
full_wifi_ota = False
|
||||
@@ -244,6 +247,8 @@ for section, options in data:
|
||||
ota_disabled = True
|
||||
if "OTA_SD_STORE" in str(flag):
|
||||
sd_ota = 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)
|
||||
@@ -256,6 +261,7 @@ for section, options in data:
|
||||
f"{env_name}\t{platform}\t{1 if mqtt_enabled else 0}"
|
||||
f"\t{1 if ota_enabled and not ota_disabled else 0}"
|
||||
f"\t{1 if sd_ota else 0}"
|
||||
f"\t{1 if qspi_ota else 0}"
|
||||
f"\t{1 if full_enabled else 0}\t{1 if full_wifi_ota else 0}"
|
||||
f"\t{board_value}"
|
||||
)
|
||||
@@ -290,6 +296,7 @@ for section, options in data:
|
||||
PIO_ENV_MQTT_BY_NAME["$ota_env"]=0
|
||||
PIO_ENV_OTA_BY_NAME["$ota_env"]=1
|
||||
PIO_ENV_SD_OTA_BY_NAME["$ota_env"]="${PIO_ENV_SD_OTA_BY_NAME[$env_name]:-0}"
|
||||
PIO_ENV_QSPI_OTA_BY_NAME["$ota_env"]="${PIO_ENV_QSPI_OTA_BY_NAME[$env_name]:-0}"
|
||||
PIO_ENV_FULL_BUILD_BY_NAME["$ota_env"]=0
|
||||
PIO_ENV_FULL_WIFI_OTA_BY_NAME["$ota_env"]=0
|
||||
PIO_ENV_BUILD_BASE_BY_NAME["$ota_env"]="$env_name"
|
||||
@@ -313,6 +320,13 @@ for section, options in data:
|
||||
[ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" = "ESP32_PLATFORM" ] || continue
|
||||
|
||||
full_env=${env_name/companion_radio_wifi/companion_radio_full}
|
||||
# This FEM-enabled image auto-detects both the GC1109 used by Heltec V4.2
|
||||
# and the KCT8103L used by V4.3. Keep both revisions in the generated
|
||||
# target and artifact name so users do not mistake it for a V4.0-only
|
||||
# build. The slash used in the display label is not valid in a filename.
|
||||
if [ "$full_env" = "heltec_v4_companion_radio_full_femon" ]; then
|
||||
full_env=heltec_v4_2_v4_3_companion_radio_full_femon
|
||||
fi
|
||||
usb_env=${env_name/companion_radio_wifi/companion_radio_usb}
|
||||
ble_env=${env_name/companion_radio_wifi/companion_radio_ble}
|
||||
if [ -n "${PIO_ENV_PLATFORM_BY_NAME[$full_env]+x}" ] \
|
||||
@@ -329,6 +343,7 @@ for section, options in data:
|
||||
PIO_ENV_MQTT_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_OTA_BY_NAME["$full_env"]=1
|
||||
PIO_ENV_SD_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_QSPI_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_FULL_BUILD_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_FULL_WIFI_OTA_BY_NAME["$full_env"]="${PIO_ENV_FULL_WIFI_OTA_BY_NAME[$env_name]:-0}"
|
||||
PIO_ENV_BUILD_BASE_BY_NAME["$full_env"]="$env_name"
|
||||
@@ -360,6 +375,7 @@ for section, options in data:
|
||||
PIO_ENV_MQTT_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_OTA_BY_NAME["$full_env"]=1
|
||||
PIO_ENV_SD_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_QSPI_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_FULL_BUILD_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_FULL_WIFI_OTA_BY_NAME["$full_env"]=0
|
||||
PIO_ENV_BUILD_BASE_BY_NAME["$full_env"]="$env_name"
|
||||
@@ -1457,6 +1473,12 @@ print_release_firmware_targets() {
|
||||
;;
|
||||
get-repeater-firmwares-to-build)
|
||||
get_pio_envs_ending_with_string "_repeater"
|
||||
# This full-sensor target is a distinct hardware/bootloader contract,
|
||||
# not a generated lean OTA alias, so tagged repeater releases must ship
|
||||
# it explicitly alongside the canonical standard repeaters.
|
||||
if is_supported_build_env "RAK_4631_repeater_rak15001_slot_c_lora_ota"; then
|
||||
printf '%s\n' "RAK_4631_repeater_rak15001_slot_c_lora_ota"
|
||||
fi
|
||||
;;
|
||||
get-room-server-firmwares-to-build)
|
||||
get_pio_envs_ending_with_string "_room_server"
|
||||
@@ -1554,6 +1576,11 @@ is_lora_ota_only_target() {
|
||||
[[ "$target_lc" == *lora_ota* ]]
|
||||
}
|
||||
|
||||
is_lora_ota_no_external_sensors_target() {
|
||||
local target_lc=${1,,}
|
||||
[[ "$target_lc" == *lora_ota_no_external_sensors ]]
|
||||
}
|
||||
|
||||
filter_out_lora_ota_only_targets() {
|
||||
local target
|
||||
|
||||
@@ -2043,10 +2070,11 @@ is_lora_ota_build() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The OTA manager, staging store, and self-install path are deliberately opt-in. The standard repeater
|
||||
# remains a normal, sensor-enabled build, but Mesh transport still relays OTA floods opaquely during
|
||||
# TempRadio. Its explicit _lora_ota_no_external_sensors sibling is the constrained self-updatable image.
|
||||
if [[ "$env_name_lc" != *lora_ota_no_external_sensors ]]; then
|
||||
# The OTA manager, staging store, and self-install path are deliberately
|
||||
# opt-in. Most boards use the constrained no-external-sensors sibling. A
|
||||
# purpose-built external-QSPI target may retain the full board feature set.
|
||||
if ! is_lora_ota_no_external_sensors_target "$env_name" \
|
||||
&& [ "${PIO_ENV_QSPI_OTA_BY_NAME[$env_name]:-0}" != "1" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -2268,7 +2296,8 @@ apply_nrf52_size_profile() {
|
||||
apply_lora_ota_no_external_sensors_profile() {
|
||||
local env_name=$1
|
||||
|
||||
if ! is_lora_ota_build "$env_name" || ! is_lora_ota_only_target "$env_name"; then
|
||||
if ! is_lora_ota_build "$env_name" \
|
||||
|| ! is_lora_ota_no_external_sensors_target "$env_name"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -2378,7 +2407,10 @@ apply_lora_ota_override() {
|
||||
fi
|
||||
|
||||
if is_lora_ota_build "$env_name"; then
|
||||
if [ "${PIO_ENV_SD_OTA_BY_NAME[$env_name]:-0}" = "1" ]; then
|
||||
if [ "${PIO_ENV_QSPI_OTA_BY_NAME[$env_name]:-0}" = "1" ]; then
|
||||
append_platformio_build_unflags "-UENABLE_OTA -DDISABLE_LORA_OTA=1 -DOTA_FLASH_STORE=1 -DOTA_SD_STORE=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -UOTA_FLASH_STORE -UOTA_SD_STORE -DOTA_QSPI_STORE=1 -DOTA_FOLDER_SERIAL"
|
||||
elif [ "${PIO_ENV_SD_OTA_BY_NAME[$env_name]:-0}" = "1" ]; then
|
||||
append_platformio_build_unflags "-UENABLE_OTA -DDISABLE_LORA_OTA=1 -DOTA_FLASH_STORE=1"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -UOTA_FLASH_STORE -DOTA_SD_STORE=1 -DOTA_FOLDER_SERIAL"
|
||||
else
|
||||
|
||||
@@ -41,7 +41,7 @@ retain 50 because their MQTT discovery tables are constrained by internal DRAM.
|
||||
| ESP32 MQTT observer or ESP-NOW bridge | Always uses the expanded FULL partition profile. The build never substitutes a reduced CLI to fit the legacy application slot. |
|
||||
| FULL ESP32 | Uses the matching MQTT target with logging off and keeps the complete command surface supported by that role and hardware. |
|
||||
| FULL ESP32 logging | Uses the matching non-MQTT target with debug and packet logging enabled, session-only `get/set usb.logging` control, and the complete command surface supported by that role and hardware. |
|
||||
| `no_external_sensors` | Removes optional external-sensor drivers and their settings; it does not remove core repeater discovery or routing commands. |
|
||||
| `no_external_sensors` | Removes optional external-sensor drivers and their settings; it does not remove core repeater discovery or routing commands. On RAK3401, this profile also compiles out GPS, so RAK12501 requires the ordinary full-sensor build. |
|
||||
|
||||
`logging`, `OTA`, and `FULL` describe independent build features. Do not infer
|
||||
that a command is missing merely because `logging` appears in the filename.
|
||||
|
||||
@@ -163,7 +163,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| LoRa OTA | [`ota`; `ota status`; `ota st`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes |
|
||||
| LoRa OTA | [`ota stats`](ota_user_guide.md#1-see-what-im-running-and-whether-anything-is-going-on) | LoRa OTA build | No | No | Yes |
|
||||
| LoRa OTA | [`ota ls`; `ota neighbors`; `ota nbrs`; `ota updates`; `ota n`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes |
|
||||
| LoRa OTA | [`ota get`; `ota pull`; `ota download`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build; nRF52 installs in-place deltas | No | No | Yes |
|
||||
| LoRa OTA | [`ota get`; `ota pull`; `ota download`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build; internal-store nRF52 installs in-place deltas, while matched SD/QSPI nRF52 also accepts full images | No | No | Yes |
|
||||
| LoRa OTA | [`ota install`; `ota apply`; `ota applydelta`](ota_protocol.md#11-cli-surface-otaclicpp) | Compatible bootloader and completed update | No | No | Yes |
|
||||
| LoRa OTA | [`ota rescue install <base_hash16>`](ota_protocol.md#12-apply-bootloader-contract) | Internal-flash nRF52 LoRa OTA build with failed app-side EndF validation | No | No | Feature |
|
||||
| LoRa OTA | [`ota cancel`; `ota drop`; `ota stop`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes |
|
||||
|
||||
+2
-1
@@ -9,8 +9,9 @@ Below are a few quick start guides.
|
||||
- [Filter Policy Playground](./filter_tool.md)
|
||||
- [Telemetry Decoder](./telemetry_decoder.md)
|
||||
- [CLI Availability by Firmware Build](./cli_build_matrix.md)
|
||||
- [Easy LoRa OTA: ESP32 full images and nRF52 deltas](./ota_easy.md)
|
||||
- [Easy LoRa OTA: ESP32 and nRF52 firmware updates](./ota_easy.md)
|
||||
- [Scripted LoRa OTA: Bash and PowerShell](./lora_ota_automation.md)
|
||||
- [nRF52 repeater OTA with external QSPI](./ota_nrf52_qspi.md)
|
||||
- [MeshTower V2 microSD self-updates](./ota_meshtower_v2_sdcard.md)
|
||||
- [GPS Tracking](./gps_tracking.md)
|
||||
- [Companion Protocol](./companion_protocol.md)
|
||||
|
||||
@@ -61,6 +61,7 @@ while the same link carries framed folder requests. BLE remains available.
|
||||
| --- | --- | --- | --- |
|
||||
| ESP32 | Full application image | OTA-enabled image with an A/B partition table | Builds a full mOTA from the matching non-merged application `.bin` |
|
||||
| nRF52, internal flash | In-place delta | Exact-board OTAFIX bootloader with mOTA apply support | Requires `--base` with the exact image currently running |
|
||||
| nRF52 repeater, external QSPI | Full image or in-place delta | QSPI-aware exact-board OTAFIX bootloader and matched repeater build | Builds a full mOTA; adding `--base` requests a delta |
|
||||
| MeshTower V2 nRF52, microSD | Full image or in-place delta | SD-aware exact-board OTAFIX bootloader and compatible card | Builds a full mOTA; adding `--base` requests a delta |
|
||||
|
||||
The firmware inside a raw ZIP must have a valid MeshCore `EndF` trailer. An
|
||||
@@ -68,7 +69,7 @@ ESP32 merged/factory image is not an application image and is rejected. A
|
||||
generic vendor DFU ZIP may also be unusable if it does not contain the raw
|
||||
EndF-bearing `.hex` or `.bin`.
|
||||
|
||||
For an internal-flash nRF52, the exact base image is irreducible information.
|
||||
For an internal-staging nRF52, the exact base image is irreducible information.
|
||||
The node reports its eight-byte body hash, but that hash cannot reconstruct the
|
||||
firmware bytes needed to create a delta. Keep the `.pio/build/ENV/firmware.hex`
|
||||
that was actually flashed. A matching filename or version alone is not enough.
|
||||
@@ -177,7 +178,8 @@ ota stats
|
||||
|
||||
The script uses `get bootloader.ver` to distinguish ESP32 from nRF52 and, for
|
||||
nRF52, report the installed bootloader version. It then requires `ota self` to
|
||||
report `bootloader: apply OK` or `bootloader: SD apply OK` and checks the
|
||||
report `bootloader: apply OK`, `bootloader: QSPI apply OK`, or
|
||||
`bootloader: SD apply OK` and checks the
|
||||
reported bootloader ABI and codec mask against the selected package. If the
|
||||
version command is unavailable on older firmware, the script warns and falls
|
||||
back to the legacy `ota self` platform marker. If an nRF52 bootloader lacks
|
||||
@@ -238,7 +240,7 @@ Prefer the environment variable or the interactive password prompt. Passing
|
||||
local processes. The runner keeps the password out of child `meshcli` command
|
||||
lines and removes its protected temporary command file after each call.
|
||||
|
||||
## 5. Run an internal-flash nRF52 update
|
||||
## 5. Run an nRF52 update
|
||||
|
||||
If the input ZIP already contains a compatible in-place delta `.mota`, no
|
||||
base argument is needed: its embedded base hash is compared with the live
|
||||
@@ -258,16 +260,25 @@ node. If the ZIP contains raw new firmware, supply the exact running image:
|
||||
--source-serial COM8
|
||||
```
|
||||
|
||||
Before building the delta, the runner proves that the base's target ID,
|
||||
Before building a delta, the runner proves that the base's target ID,
|
||||
hardware identity, firmware version when available, and `EndF` body hash match
|
||||
the live destination. It then asks `motatool` for codec 2, the nRF52 in-place
|
||||
format. The normal workspace is `0x98000`.
|
||||
|
||||
For the SD-backed MeshTower V2 target, a raw ZIP becomes a full image without
|
||||
`--base`. Supplying an exact base requests a smaller in-place delta and
|
||||
automatically selects its `0xC7000` workspace. An explicit
|
||||
For a QSPI-backed repeater or the SD-backed MeshTower V2 target, a raw ZIP
|
||||
becomes a full image without `--base`. Supplying an exact base requests a
|
||||
smaller in-place delta and automatically selects the conservative external
|
||||
workspace `0xC6000`, which is safe for S140 v6 and v7 application layouts. An explicit
|
||||
`--inplace-memory` overrides the automatic value.
|
||||
|
||||
The live runner detects QSPI from `ota self` (`QSPI apply OK`) or `ota status`
|
||||
(`bl:QSPI`) and refuses an explicit `QSPI store:ERR 0K` report even when the
|
||||
bootloader itself advertises QSPI apply support. Offline `--prepare-only` runs
|
||||
must supply `--nrf-qspi`; do not use that switch for a board that only exposes
|
||||
QSPI pins or uses the chip as a
|
||||
Companion filesystem. The application and bootloader must both be from the
|
||||
matched repeater list in [the nRF52 QSPI guide](ota_nrf52_qspi.md).
|
||||
|
||||
## 6. Add intermediate relays
|
||||
|
||||
List relays from farthest to nearest so each command is sent before its route
|
||||
|
||||
+81
-37
@@ -9,12 +9,14 @@ For an end-to-end controller that accepts a release ZIP or ready mOTA, see
|
||||
| Destination | Update type | Files needed to build the `.mota` | Installer |
|
||||
| --- | --- | --- | --- |
|
||||
| ESP32 | Full firmware | New non-merged application `.bin` | ESP32 A/B firmware slots |
|
||||
| nRF52 | In-place delta | Exact running `firmware.hex` and new `firmware.hex` | Exact-board OTAFIX bootloader |
|
||||
| nRF52, internal staging | In-place delta | Exact running `firmware.hex` and new `firmware.hex` | Exact-board OTAFIX bootloader |
|
||||
| Supported nRF52 QSPI repeater | Full firmware or in-place delta | New `firmware.hex`; a delta also needs the exact running `firmware.hex` | Matching QSPI-aware OTAFIX bootloader |
|
||||
| MeshTower V2 SD target | Full firmware or in-place delta | New `firmware.hex`; a delta also needs the exact running `firmware.hex` | Matching SD-aware OTAFIX bootloader |
|
||||
|
||||
A normal nRF52 target cannot install a full-image container. It deliberately accepts only an in-place
|
||||
delta built against its exact running firmware. The MeshTower V2 microSD target is the exception because
|
||||
it stages the complete container off-chip; see [MeshTower V2 microSD LoRa OTA](ota_meshtower_v2_sdcard.md).
|
||||
An internal-staging nRF52 target accepts only an in-place delta built against its exact running firmware.
|
||||
Matched QSPI repeater targets and the MeshTower V2 microSD target can also install a full image because the
|
||||
complete container stays off-chip. See [nRF52 repeater OTA with external QSPI](ota_nrf52_qspi.md) and
|
||||
[MeshTower V2 microSD LoRa OTA](ota_meshtower_v2_sdcard.md).
|
||||
|
||||
## Temporary OTA channel used in this guide
|
||||
|
||||
@@ -44,10 +46,11 @@ configurations. Confirm that it is permitted in your location and change it when
|
||||
|
||||
Both paths require:
|
||||
|
||||
- An OTA-enabled build whose artifact filename contains `-ota-` on the destination. The `-ota-` stamp confirms
|
||||
that the node can discover, download, verify, and install LoRa OTA. Intermediate repeaters do **not** need an
|
||||
OTA-enabled build: current repeater firmware relays OTA packets opaquely without storing or installing them.
|
||||
Standard logging and untagged builds cannot install LoRa OTA; FULL MQTT and FULL logging OTA builds can.
|
||||
- A destination artifact explicitly identified by its release table as install-capable. Some lean internal
|
||||
nRF52 builds carry `lora_ota_no_external_sensors` in the filename, while matched QSPI boards enable install
|
||||
support in the normal full-sensor repeater artifact, so filename text alone is not authoritative. Confirm
|
||||
support with `ota self` and `ota status`. Intermediate repeaters do **not** need an install-capable build:
|
||||
current repeater firmware relays OTA packets opaquely without storing or installing them.
|
||||
- An OTA-enabled MeshCore source connected to the computer by USB serial, or
|
||||
an ESP32 WiFi companion/FULL source connected over WiFi as described below.
|
||||
- Overlapping `tempradio` windows on the source, destination, and every repeater needed between them.
|
||||
@@ -57,14 +60,23 @@ apply their normal forwarding filters, duplicate checks, and flood limits; they
|
||||
payload. If any required window closes, the transfer stops making progress and can resume during a later
|
||||
overlapping window.
|
||||
|
||||
`build.sh` provides a `*_repeater_lora_ota_no_external_sensors` build for every standalone ESP32 and nRF52
|
||||
repeater target. The normal repeater build keeps its external-sensor support and can serve as an intermediate
|
||||
OTA relay, but it cannot download or install an update for itself. The `-ota-` sibling omits optional external
|
||||
I2C environmental sensors to preserve the update workspace, while retaining board-native features such as its
|
||||
display, buttons, battery monitoring, and integrated GPS. ESP32 `-ota-` siblings also retain the lightweight
|
||||
browser WiFi uploader (`start ota`), the complete CLI, and a 254-entry neighbor table. RP2040 and STM32 repeaters do not
|
||||
currently have a safe self-apply path, but current repeater firmware can still relay OTA packets opaquely
|
||||
during TempRadio.
|
||||
`build.sh` provides a `*_repeater_lora_ota_no_external_sensors` build for standalone ESP32 and nRF52 repeater
|
||||
targets that need a smaller internal update workspace. Those siblings omit optional external I2C
|
||||
environmental sensors while retaining board-native features such as displays, buttons, battery monitoring,
|
||||
and integrated GPS where the target uses the GPS-preserving lean profile. The
|
||||
RAK3401 target is an explicit exception:
|
||||
`RAK_3401_repeater_lora_ota_no_external_sensors` compiles out GPS and cannot
|
||||
use a RAK12501. Use the ordinary full-sensor `RAK_3401_repeater` build with a
|
||||
RAK12501 in sensor slot A when GPS is required; slot D conflicts with the
|
||||
RAK13302 radio's BUSY/DIO1 lines. Selected nRF52 boards with matched external
|
||||
QSPI application and bootloader support can instead make the normal full-sensor
|
||||
repeater install-capable; those targets do not need to reserve internal flash
|
||||
for the downloaded container. Other normal repeaters can still serve as
|
||||
intermediate relays but cannot necessarily install an update themselves. ESP32
|
||||
`-ota-` siblings also retain the lightweight browser WiFi uploader (`start
|
||||
ota`), the complete CLI, and a 254-entry neighbor table. RP2040 and STM32
|
||||
repeaters do not currently have a safe self-apply path, but current repeater
|
||||
firmware can still relay OTA packets opaquely during TempRadio.
|
||||
|
||||
nRF52 `-ota-` siblings are compiled with size optimization instead of the Adafruit platform's default
|
||||
speed optimization. This prevents the retained software Ed25519 fallback from expanding beyond the fixed
|
||||
@@ -153,16 +165,17 @@ identity trailer so `motatool` and the destination can verify the target, hardwa
|
||||
|
||||
Do not continue if `motatool verify` reports a failure.
|
||||
|
||||
## nRF52: package an in-place delta
|
||||
## nRF52: package a full image or in-place delta
|
||||
|
||||
### 1. Install and check the OTAFIX bootloader
|
||||
|
||||
This is a one-time prerequisite. Install the OTAFIX bootloader built for the destination's **exact board**
|
||||
from the
|
||||
[OTAFIX 2.4 nRF52 bootloader release](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/0.9.2-OTAFIX2.4).
|
||||
Follow the release's board-specific installation and erase instructions. If it does not contain the
|
||||
destination's exact board, this LoRa install path is not yet available for that board; never substitute a
|
||||
similar board's bootloader.
|
||||
[OTAFIX nRF52 bootloader releases](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases).
|
||||
Internal-staging delta updates require OTAFIX 2.4 or newer. External QSPI and SD staging require OTAFIX
|
||||
2.4.1 or newer and release notes that explicitly list the exact board and storage mode. Follow the release's
|
||||
board-specific installation and erase instructions. If it does not contain the destination's exact board,
|
||||
this LoRa install path is not yet available for that board; never substitute a similar board's bootloader.
|
||||
|
||||
Before preparing or downloading a LoRa update, run this on the destination:
|
||||
|
||||
@@ -172,17 +185,33 @@ ota self
|
||||
```
|
||||
|
||||
The first command identifies the installed nRF52 bootloader. Continue only if
|
||||
the `ota self` reply includes:
|
||||
the `ota self` reply includes the apply mode required by this target:
|
||||
|
||||
```text
|
||||
bootloader: apply OK
|
||||
bootloader: QSPI apply OK
|
||||
bootloader: SD apply OK
|
||||
```
|
||||
|
||||
The reply also contains the running firmware's `base_hash`. Save it for the package check below. A stock,
|
||||
legacy, or older OTAFIX bootloader without `.mota` in-place-apply support will report that apply support is
|
||||
missing, and `ota install` will refuse to reboot into it.
|
||||
|
||||
### 2. Keep the exact current and new application images
|
||||
### 2. Choose full or delta packaging
|
||||
|
||||
For a QSPI- or SD-backed target, a full update needs only the new raw
|
||||
`firmware.hex`:
|
||||
|
||||
```bash
|
||||
mkdir -p ./motas
|
||||
motatool build --fw ./LilyGo_T-Echo_repeater-new.hex --out-dir ./motas
|
||||
motatool verify ./motas/*.mota
|
||||
```
|
||||
|
||||
Use a full package when an exact base image is unavailable. Use the delta path
|
||||
below when reducing airtime is worth retaining the exact running image.
|
||||
|
||||
### 3. Keep the exact current and new application images for a delta
|
||||
|
||||
You need the raw `.pio/build/<environment>/firmware.hex` from the build that is **actually running**, plus
|
||||
the corresponding `firmware.hex` from the new build. Save the current file before building the new version,
|
||||
@@ -190,13 +219,16 @@ because PlatformIO reuses that path. For example:
|
||||
|
||||
```bash
|
||||
# Save this immediately after building/flashing the version now running on the node.
|
||||
cp .pio/build/Heltec_t114_repeater/firmware.hex ./Heltec_t114_repeater-running.hex
|
||||
cp .pio/build/Heltec_t114_repeater_lora_ota_no_external_sensors/firmware.hex \
|
||||
./Heltec_t114_repeater_lora_ota_no_external_sensors-running.hex
|
||||
|
||||
# After checking out and building the new version, save its image separately.
|
||||
cp .pio/build/Heltec_t114_repeater/firmware.hex ./Heltec_t114_repeater-new.hex
|
||||
cp .pio/build/Heltec_t114_repeater_lora_ota_no_external_sensors/firmware.hex \
|
||||
./Heltec_t114_repeater_lora_ota_no_external_sensors-new.hex
|
||||
```
|
||||
|
||||
Replace `Heltec_t114_repeater` with the destination's exact PlatformIO environment. The two images must be
|
||||
Replace `Heltec_t114_repeater_lora_ota_no_external_sensors` with the destination's exact PlatformIO
|
||||
environment. The two images must be
|
||||
for the same board and role, and both must contain their `EndF` trailers. Do not pass a release `.uf2` or
|
||||
BLE-DFU `.zip` to `motatool`; those are installation containers rather than raw application images.
|
||||
|
||||
@@ -206,14 +238,19 @@ running application. The hash check in the next step proves that it is the right
|
||||
On RAK4631 repeaters, use the
|
||||
`RAK_4631_repeater_lora_ota_no_external_sensors` environment. It retains built-in battery monitoring but
|
||||
omits optional external environmental sensor packages so the delta fits the safe in-place workspace.
|
||||
If the device has a RAK15001 installed in sensor slot C and the matching
|
||||
RAK15001 OTAFIX bootloader, use
|
||||
`RAK_4631_repeater_rak15001_slot_c_lora_ota` instead. That target retains the
|
||||
full sensor/GPS set and can install either a full image or a delta from the
|
||||
external 2 MiB store.
|
||||
|
||||
### 3. Build and check the in-place delta
|
||||
### 4. Build and check the in-place delta
|
||||
|
||||
```bash
|
||||
mkdir -p ./motas
|
||||
motatool build \
|
||||
--base ./Heltec_t114_repeater-running.hex \
|
||||
--fw ./Heltec_t114_repeater-new.hex \
|
||||
--base ./Heltec_t114_repeater_lora_ota_no_external_sensors-running.hex \
|
||||
--fw ./Heltec_t114_repeater_lora_ota_no_external_sensors-new.hex \
|
||||
--patch-type in-place \
|
||||
--out-dir ./motas
|
||||
motatool verify ./motas/*.mota
|
||||
@@ -233,9 +270,12 @@ Check all three of these before serving it:
|
||||
firmware version identify the intended board and role. If `inspect` shows `N/A` for the human-readable
|
||||
target name, the tool's name table is older than that environment; the numeric IDs still must match.
|
||||
|
||||
The default `--inplace-memory 0x98000` and 4096-byte segment size match the supported MeshCore OTAFIX
|
||||
builds; do not override them for this normal nRF52 flow. Do not continue if verification or any identity
|
||||
check fails.
|
||||
Current layout-aware `motatool` derives the safe workspace from the new firmware's authenticated EndF layout
|
||||
record, so the normal recipe deliberately omits `--inplace-memory`. If older tooling requires an explicit
|
||||
override, use `0xC6000` only for matched external SD/QSPI staging and `0x98000` for an internal-staging nRF52
|
||||
such as the RAK4631 lean OTA build. Do not substitute the external value on an internal target, and do not
|
||||
override either value unless you have verified the exact app base, bootloader, and package geometry. Do not
|
||||
continue if verification or any identity check fails.
|
||||
|
||||
## Transfer and install either package
|
||||
|
||||
@@ -290,8 +330,9 @@ ota ls
|
||||
```
|
||||
|
||||
Discovery is asynchronous. `ota ls` says `refreshing`; wait a few seconds and run it again even if it first
|
||||
shows an older row. Select `[same target]`: it should say `full` for the ESP32 path or `delta` for the nRF52
|
||||
path. Do not select `[unsupported]` (for example, a source's self-served full image on a single-slot nRF52).
|
||||
shows an older row. Select `[same target]`: it can say `full` for ESP32 and external SD/QSPI nRF52 targets,
|
||||
or `delta` for any supported nRF52 target. Do not select `[unsupported]` (for example, a source's self-served
|
||||
full image on an internal-staging nRF52).
|
||||
Use the row's stable eight-hex manifest ID rather than its changing list position:
|
||||
|
||||
```text
|
||||
@@ -335,8 +376,9 @@ ota install
|
||||
```
|
||||
|
||||
The destination verifies the complete package again before approving it. ESP32 installs the full image into
|
||||
its inactive A/B slot. nRF52 checks the base hash and bootloader capability, then reboots into OTAFIX; the
|
||||
bootloader independently rechecks the package, applies the delta in place, and verifies the resulting image.
|
||||
its inactive A/B slot. nRF52 checks the bootloader and storage capabilities and, for a delta, the base hash,
|
||||
then reboots into OTAFIX. The bootloader independently rechecks the package, installs the external full image
|
||||
or applies the delta in place, and verifies the resulting image.
|
||||
Pre-install failures leave the running firmware unchanged and report the reason. If power is lost after an
|
||||
nRF52 in-place apply has begun, OTAFIX will not boot a partial image; it enters recovery DFU so a known-good
|
||||
application can be restored.
|
||||
@@ -357,7 +399,9 @@ ota status
|
||||
- **The update shows another environment or a raw `[hw XXXXXXXX]`:** it is for a different board or firmware
|
||||
role. Do not install it.
|
||||
- **An internal-flash nRF52 marks a full update `[unsupported]`:** it can install only an in-place delta.
|
||||
The MeshTower V2 microSD target accepts full images with its matching SD-aware bootloader.
|
||||
A matched QSPI repeater or MeshTower V2 microSD target accepts full images with its corresponding bootloader.
|
||||
- **A QSPI nRF52 reports `QSPI store:ERR 0K` or `bl:NO-QSPI`:** do not download an install package. Install
|
||||
the exact QSPI-aware bootloader and check that the selected application matches the board's flash wiring.
|
||||
- **nRF52 reports no bootloader apply support:** install the exact-board in-place-delta OTAFIX bootloader
|
||||
before trying LoRa OTA.
|
||||
- **nRF52 reports a base mismatch:** the file passed to `--base` is not the exact application running on
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
# nRF52 repeater LoRa OTA with external QSPI
|
||||
|
||||
Selected nRF52840 repeater builds use their dedicated external QSPI NOR flash as
|
||||
a raw LoRa OTA staging device. This removes the internal-flash staging conflict:
|
||||
the complete `.mota` stays off-chip, and the bootloader can use the entire
|
||||
internal application region while it installs either a full image or an
|
||||
in-place delta.
|
||||
|
||||
This is a matched application-and-bootloader feature. A board merely having an
|
||||
nRF52840, free RAM, or pins named QSPI is not enough. Both halves must use the
|
||||
exact flash wiring, and `ota self` must confirm the store and bootloader before
|
||||
an update is downloaded.
|
||||
|
||||
## Supported repeater families
|
||||
|
||||
QSPI staging is enabled only for repeater-role environments on these currently
|
||||
matched families:
|
||||
|
||||
- Seeed XIAO nRF52840 and XIAO nRF52840 Sense modules, including the
|
||||
`Xiao_nrf52`, SolarXiao 30S/33S, and XIAO-module Ikoka handheld, Nano, and
|
||||
Stick repeaters
|
||||
- original LilyGo T-Echo
|
||||
- Elecrow ThinkNode M1 and M6
|
||||
- Seeed Wio Tracker L1
|
||||
- Seeed SenseCAP Solar Node P1
|
||||
- RAK4631 with a RAK15001 in WisBlock sensor slot C, using the dedicated
|
||||
`RAK_4631_repeater_rak15001_slot_c_lora_ota` application and matching
|
||||
`wiscore_rak4631_board_rak15001_slot_c` OTAFIX bootloader
|
||||
|
||||
Heltec T114 is intentionally not in this list. Its public V1, V2.0, and V2.1
|
||||
schematics show U9 (MX25R1635F) as an optional QSPI footprint, so standard T114
|
||||
application and bootloader targets do not assume that external NOR is populated.
|
||||
|
||||
The XIAO-module derivatives use the matching XIAO or XIAO Sense OTAFIX
|
||||
bootloader shown by the module's `INFO_UF2.TXT`. Do not substitute a similar
|
||||
bootloader for a board with different QSPI pins. For example, the T-Echo Card
|
||||
and T-Echo Lite have different flash wiring and are not enabled by the original
|
||||
T-Echo target.
|
||||
|
||||
Companion builds are intentionally excluded. Some companion targets use the
|
||||
same external QSPI as a LittleFS message/data store, where raw OTA staging
|
||||
would corrupt the filesystem; other companions simply do not assign that chip
|
||||
to OTA. Room-server, sensor, KISS, and repeater-bridge roles are also unchanged.
|
||||
Raw QSPI OTA is scoped to the explicitly matched repeater environments.
|
||||
|
||||
### RAK15001 placement and module conflicts
|
||||
|
||||
[RAK15001](https://docs.rakwireless.com/product-categories/wisblock/rak15001/datasheet/)
|
||||
is a 2 MiB GD25Q16C **standard SPI** module, not a quad-I/O flash. The dedicated
|
||||
RAK4631 target uses the nRF52840 QSPI peripheral in its single-data-line
|
||||
FAST_READ/page-program modes at 8 MHz and accepts only the module's exact
|
||||
`C8 40 15` JEDEC ID. `ota self` reports `QSPI store:2048K` only when the
|
||||
expected module responds. An empty slot or a different SPI device reports
|
||||
`QSPI store:ERR 0K`, and install is refused.
|
||||
|
||||
The module is electrically usable in sensor slot A-D because those slots share
|
||||
the SPI signals and RAK15001 has onboard 10 kOhm pull-ups on WP# and HOLD#.
|
||||
The supported MeshCore/OTAFIX combination nevertheless requires **slot C**. It
|
||||
is the placement that remains safe when a
|
||||
[RAK12501 GNSS](https://docs.rakwireless.com/product-categories/wisblock/rak12501/datasheet/)
|
||||
is fitted in either of its supported slots, A or D: slot C avoids the GNSS
|
||||
PPS/reset nets on IO1/IO2 and IO5/IO6. In particular, GPS in A plus flash in B
|
||||
would share IO1/IO2 through the modules' auxiliary pins.
|
||||
|
||||
Only one device that uses the shared WisBlock SPI chip-select may be fitted.
|
||||
Do not combine this target with RAK13800 Ethernet, RAK15002 SD, or another SPI
|
||||
module. RAK13800 and RAK15001 cannot coexist because they use the same SPI
|
||||
chip-select. Update a RAK13800 Ethernet build locally over USB using the
|
||||
release's **Manual UF2** or **Serial DFU (.zip)** download; it cannot use this
|
||||
RAK15001 LoRa-OTA staging target.
|
||||
|
||||
RAK3401 is intentionally unsupported. Its external RAK13302 1 W radio already
|
||||
uses the same WisBlock SPI clock/data pins **and the same chip-select** as
|
||||
RAK15001. Firmware cannot independently select or detect the two chips, so a
|
||||
stock RAK3401 + RAK15001 assembly cannot provide reliable OTA staging without
|
||||
a hardware chip-select rework.
|
||||
|
||||
## One-time prerequisite
|
||||
|
||||
Install a QSPI-capable OTAFIX 2.4.1 preview.8 or newer bootloader for the exact
|
||||
board from the
|
||||
[OTAFIX releases](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases)
|
||||
before using LoRa OTA. The release notes must explicitly list that board's QSPI
|
||||
mode. Also install the SoftDevice version expected by that target. The
|
||||
application refuses the install handoff when the bootloader does not advertise
|
||||
QSPI support.
|
||||
|
||||
For the first migration from the ordinary `wiscore_rak4631_board` bootloader to
|
||||
`wiscore_rak4631_board_rak15001_slot_c`, use Nordic serial DFU or a compatible
|
||||
BLE DFU client with the exact slot-C OTAFIX **combined bootloader + SoftDevice
|
||||
DFU package** (or use SWD), then reinstall the slot-C MeshCore application. The
|
||||
release filename has this form:
|
||||
|
||||
```text
|
||||
wiscore_rak4631_board_rak15001_slot_c_bootloader-<OTAFIX-version>_s140_6.1.1.zip
|
||||
```
|
||||
|
||||
Do not copy the slot-C bootloader-update UF2 onto the stock UF2 drive. The
|
||||
stock loader is bound to `DEVICE_NAME=4631_DFU`, while the slot-C image is bound
|
||||
to `4631_15001C_DFU`, so that UF2 is intentionally rejected. After the one-time
|
||||
DFU/SWD migration, later canonical slot-C bootloader UF2 files work normally.
|
||||
The combined OTAFIX package above is not the MeshCore application's Serial DFU
|
||||
`.zip`; an application package does not migrate the bootloader.
|
||||
|
||||
After installing the repeater application, check:
|
||||
|
||||
```text
|
||||
get bootloader.ver
|
||||
ota self
|
||||
ota status
|
||||
```
|
||||
|
||||
A ready target reports all of the following:
|
||||
|
||||
```text
|
||||
QSPI store:2048K
|
||||
bootloader: QSPI apply OK
|
||||
bl:QSPI
|
||||
```
|
||||
|
||||
Other supported boards can report a capacity different from 2048K; the
|
||||
RAK15001 target must report exactly 2048K. `QSPI store:ERR 0K`, `NO QSPI`, or
|
||||
`bl:NO-QSPI` means the flash wiring, flash power, or bootloader does not match.
|
||||
Do not start an install in that state.
|
||||
|
||||
## Capacity and package types
|
||||
|
||||
The store reads the JEDEC capacity at runtime and accepts supported 1 MiB
|
||||
through 16 MiB devices using 24-bit addressing. QSPI capacity is not the final
|
||||
firmware limit. The reconstructed application, including its 56-byte `EndF`
|
||||
trailer, must fit below InternalFS at `0xED000`:
|
||||
|
||||
| SoftDevice layout | Application region | Maximum image |
|
||||
| --- | --- | --- |
|
||||
| S140 v7, app base `0x27000` | `0x27000..0xED000` | `0xC6000` (811,008 bytes) |
|
||||
| S140 v6, app base `0x26000` | `0x26000..0xED000` | `0xC7000` (815,104 bytes) |
|
||||
|
||||
A full package needs only the new raw `firmware.hex` or non-merged application
|
||||
image. An in-place delta still needs the exact image currently running. The
|
||||
automation uses a conservative `0xC6000` detools workspace for external nRF52
|
||||
staging so one package setting is safe for both layouts.
|
||||
|
||||
For online automation, QSPI is detected from `ota self` or `ota status`. For
|
||||
offline preparation, identify it explicitly:
|
||||
|
||||
```bash
|
||||
./tools/lora_ota/lora_ota.sh ./release.zip target-name \
|
||||
--prepare-only \
|
||||
--platform nrf52 \
|
||||
--nrf-qspi \
|
||||
--target-id 12345678 \
|
||||
--target-hw Xiao_nrf52
|
||||
```
|
||||
|
||||
Use the real target ID and hardware identity from the destination. A ready
|
||||
full `.mota` normally uses that target ID for discovery and routing. An
|
||||
operator can deliberately override the routing target for a role change, so
|
||||
`target_id` is not an apply-time safety assertion; the destination still
|
||||
enforces the package's hardware identity before approval.
|
||||
|
||||
## Storage ownership and recovery
|
||||
|
||||
The QSPI store is raw, not a file inside LittleFS. It owns the flash from
|
||||
offset zero, erases 4 KiB sectors as blocks arrive, writes data before progress
|
||||
metadata, and verifies every programmed page. A previously interrupted
|
||||
download is reopened only when its header and trailer are valid; every claimed
|
||||
block is re-hashed before it is trusted. Between a probe, transfer operation,
|
||||
or checkpoint, firmware puts the NOR into deep power-down, deactivates the nRF
|
||||
QSPI peripheral, and turns off a board-provided flash power-enable pin. The
|
||||
next operation powers and identifies the chip again, so merely running
|
||||
`ota self` does not leave QSPI drawing active-mode current.
|
||||
|
||||
Installing a QSPI repeater build over a former companion build therefore
|
||||
repurposes the external flash and destroys companion filesystem data as OTA
|
||||
sectors are written. Back up anything important first. Returning to a companion
|
||||
build may require formatting its external data store.
|
||||
|
||||
Before changing internal application flash, the application verifies package
|
||||
integrity, hardware identity, signature policy, and bootloader capabilities.
|
||||
The target ID selects discovery/fetch routing and can be deliberately
|
||||
overridden; it is not a second hardware gate. The bootloader then verifies a
|
||||
full payload before its first application erase. For a delta, the application
|
||||
rejects invalid detools geometry before approval and the bootloader independently
|
||||
repeats the base and geometry checks before applying it.
|
||||
It clears the one-shot approval marker before invalidating the running image.
|
||||
If power is lost after application writes begin, the bank remains invalid and
|
||||
OTAFIX enters USB/BLE recovery rather than booting a partial image.
|
||||
|
||||
See [Easy firmware updates over LoRa](ota_easy.md) for the transfer commands and
|
||||
[the OTA protocol](ota_protocol.md) for the container and handoff details.
|
||||
+45
-25
@@ -126,9 +126,13 @@ established target-specific 1920 KiB or larger A/B app layout and are checked ag
|
||||
partition. For every standalone ESP32 and nRF52 repeater, `build.sh` also exposes an explicit
|
||||
`*_lora_ota_no_external_sensors` artifact: the ordinary repeater remains sensor-enabled, while that sibling
|
||||
disables optional external environmental-sensor drivers for LoRa distribution. Integrated GPS and other
|
||||
board-native telemetry remain enabled. ESP32 siblings retain the compact browser WiFi updater and use the
|
||||
full 254-entry neighbor table. RP2040 and STM32 targets are not offered because those platforms do not yet
|
||||
have a safe bootloader/apply path.
|
||||
board-native telemetry remain enabled where the target selects the GPS-preserving lean profile. RAK3401 is
|
||||
the explicit exception: `RAK_3401_repeater_lora_ota_no_external_sensors` undefines `ENV_INCLUDE_GPS`, so it
|
||||
does not detect or use a RAK12501. RAK12501 GPS requires the ordinary full-sensor `RAK_3401_repeater` build
|
||||
and sensor slot A; slot D conflicts with the RAK13302 radio's BUSY/DIO1 lines.
|
||||
ESP32 siblings retain the compact browser WiFi updater and use the full
|
||||
254-entry neighbor table. RP2040 and STM32 targets are not offered because
|
||||
those platforms do not yet have a safe bootloader/apply path.
|
||||
|
||||
nRF52 LoRa-OTA siblings use size optimization rather than the Adafruit platform's default `-Ofast`. This
|
||||
keeps the runtime software Ed25519 fallback from being expanded into tens of kilobytes of repeated curve
|
||||
@@ -257,7 +261,7 @@ cover `approval` or `leaves[]`:
|
||||
|
||||
| `codec_id` | Meaning | Used by |
|
||||
|---|---|---|
|
||||
| 0 | full / raw | PAYLOAD = reconstructed image (`BODY||EndF`). ESP32 A/B or the SD-backed MeshTower V2 target. |
|
||||
| 0 | full / raw | PAYLOAD = reconstructed image (`BODY||EndF`). ESP32 A/B or an external SD/QSPI nRF52 target. |
|
||||
| 1 | detools **sequential** | random read of base + sequential write of result -> ESP32 A->B inactive slot. |
|
||||
| 2 | detools **in-place** | bounded scratch; rewrites the app region in place -> nRF52 single-slot. |
|
||||
|
||||
@@ -266,9 +270,9 @@ delta only if `base_hash` matches its own `EndF.body_hash`. After applying, the
|
||||
(sha2-256:32) to `image_hash` before it is booted - the hard security gate.
|
||||
|
||||
**A fetcher only requests firmware it can apply.** Each node declares the codec(s) it can apply
|
||||
(`set_apply_codec`/`set_apply_codec2`): ESP32 accepts `full` + `sequential` (+ `in-place`). Normal nRF52
|
||||
targets accept only `in-place` because internal flash cannot stage a full application image. The
|
||||
MeshTower V2 SD target accepts `full` + `in-place` because the card holds the container. A `.mota` with
|
||||
(`set_apply_codec`/`set_apply_codec2`): ESP32 accepts `full` + `sequential` (+ `in-place`). Internal-staging
|
||||
nRF52 targets accept only `in-place` because internal flash cannot hold a second full application image.
|
||||
Matched SD and raw-QSPI nRF52 targets accept `full` + `in-place` because external media holds the container. A `.mota` with
|
||||
an unsupported codec is rejected at discovery time, before any blocks are requested. A manual pull to
|
||||
an external folder may accept other codecs because that path captures bytes and never installs them.
|
||||
|
||||
@@ -599,8 +603,9 @@ transmission per hop.
|
||||
independent of signature.
|
||||
- **Signing & allowlist:** a node keeps a runtime allowlist of trusted Ed25519 signer pubkeys (none embedded
|
||||
in firmware; `ota key add/list/rm`). A `.mota` is eligible for **auto-install** only if signed by an
|
||||
allowlisted key, the signature verifies, and `image_hash` matches; otherwise it is manual-apply only with
|
||||
explicit confirmation. **Transfer needs no trust** - blocks are content-addressed against the signed root.
|
||||
allowlisted key, the signature verifies, and `image_hash` matches. Manual install permits unsigned packages,
|
||||
but a package that claims to be signed must have a valid signature from an allowlisted key or it is rejected.
|
||||
**Transfer needs no trust** - blocks are content-addressed against the manifest's merkle root.
|
||||
- **Policies (persisted):** `autofetch` in {off, any, signed} (default off) gates automatic block fetching of
|
||||
own-target adverts; `autoinstall` in {off, trusted} (default off) gates auto-apply of a COMPLETE signed +
|
||||
allowlisted fetch. Conservative defaults: a fresh node discovers + announces but never fetches/installs
|
||||
@@ -758,26 +763,31 @@ ota dev ... bring-up helpers (stage/recv/serve/verify)
|
||||
- **ESP32 (A/B):** applied in-firmware via the detools decoder into the inactive OTA slot
|
||||
(`OtaApply.cpp::ota_apply_detools_mota` + `OtaStoreFlashEsp32`), then set-boot + reboot (power-safe,
|
||||
rollback-capable). No bootloader changes. Erase ranges must be sector-aligned (4096).
|
||||
- **nRF52 (single-slot):** the running firmware **never** flashes the app. `ota applydelta` verifies fully
|
||||
(`image_hash`, `base_hash`, signature/allowlist, `hw_id`), writes `approval = "APRV"`, then reboots into
|
||||
the modified bootloader (`Adafruit_nRF52_Bootloader_OTAFIX`). The bootloader:
|
||||
1. **scans flash for `MAGIC`** to find the staged `.mota` (it must NOT trust any stored size),
|
||||
2. re-checks `TRAILER`, `image_hash`, `approval == "APRV"`, and that the delta's `base_hash` equals the
|
||||
running firmware's `EndF.body_hash` (recomputed by scanning for `EndF` - never trust `bank_0_size`),
|
||||
3. applies the in-place codec over the app region and boots only if the result hashes to `image_hash`.
|
||||
- **nRF52 internal staging ceiling:** the application derives the ceiling from facts available in every
|
||||
build, not a board-name list. A companion that actually links the internal ExtraFS datastore stays below
|
||||
`0xD4000`; a default linker region, QSPI secondary storage, or a role that does not mount ExtraFS can
|
||||
reclaim the unused 100 KiB through `0xED000`. The application uses the larger window only when the
|
||||
- **nRF52 (single-slot):** the running firmware **never** flashes the app. `ota install` verifies the
|
||||
container fully (`image_hash`, codec, signature/allowlist, `hw_id`, and `base_hash` for a delta), writes
|
||||
`approval = "APRV"`, then reboots into the modified bootloader
|
||||
(`Adafruit_nRF52_Bootloader_OTAFIX`). The bootloader:
|
||||
1. locates the staged `.mota` in the approved internal, raw-SD, or raw-QSPI store without trusting an
|
||||
unchecked stored size,
|
||||
2. re-checks `TRAILER`, `image_hash`, and `approval == "APRV"`; for a delta it also checks that
|
||||
`base_hash` equals the running firmware's `EndF.body_hash` (recomputed by scanning for `EndF` - never
|
||||
trust `bank_0_size`),
|
||||
3. writes a full external-media payload or applies the in-place codec over the app region, then boots only
|
||||
if the result hashes to `image_hash`.
|
||||
- **nRF52 internal staging ceiling:** an internal-store application derives the ceiling from facts available
|
||||
in every build, not a board-name list. A companion that actually links the internal ExtraFS datastore stays
|
||||
below `0xD4000`; a default linker region or a role that does not mount ExtraFS can reclaim the unused
|
||||
100 KiB through `0xED000`. The application uses the larger window only when the
|
||||
installed bootloader advertises the GPREGRET2 ceiling-handoff capability. The bootloader treats every
|
||||
unknown/legacy handoff value as
|
||||
`0xD4000`, and accepts a container only at the bottom-aligned position for the selected ceiling.
|
||||
- **nRF52 dynamic apply window:** the post-build hook records the resolved app base, linked app end,
|
||||
storage flags, and desired staging ceiling immediately before `EndF`. `motatool` reads that authenticated
|
||||
internal-ExtraFS/SD/QSPI storage flags, and desired staging ceiling immediately before `EndF`. `motatool` reads that authenticated
|
||||
firmware record and chooses `memory_size` from the actual patch size and bottom-aligned stage address;
|
||||
firmware without the record retains the conservative `0x98000` default. Before writing `APRV`, the app
|
||||
verifies the detools header fits below the staged container, and the bootloader independently repeats
|
||||
the geometry check before its first application write. Expanded auto-sized packages require a bootloader
|
||||
firmware without the record retains the conservative `0x98000` default. Before writing `APRV`, an
|
||||
internal-store app validates the staged-address bound; an external SD/QSPI app validates the full detools
|
||||
geometry against the application workspace. The bootloader independently parses and validates the same
|
||||
geometry before its first application write. Expanded auto-sized packages require a bootloader
|
||||
with the ceiling-handoff capability; use `--inplace-memory 0x98000` when intentionally targeting an older
|
||||
bootloader and the images still fit that window.
|
||||
- **nRF52 EndF rescue:** `ota rescue install <base_hash16>` is a pre-provisioned recovery path for an
|
||||
@@ -798,8 +808,18 @@ ota dev ... bring-up helpers (stage/recv/serve/verify)
|
||||
publishes its raw sector range in a checksummed handoff record outside the MBR partition. The matching
|
||||
bootloader reads the card without mounting FAT, supports either a full image or an in-place delta,
|
||||
verifies the staged/full result hash, and never writes through `0xED000` where InternalFS begins.
|
||||
- **Matched external-QSPI nRF52 repeaters:** the application reserves the board's dedicated QSPI NOR as a
|
||||
raw store beginning at offset zero. It obtains a 1-16 MiB capacity from JEDEC, checkpoints payload before
|
||||
leaf metadata, and verifies each erased/programmed page. GPREGRET2 `0x51` selects QSPI only when the
|
||||
matching bootloader advertises the QSPI storage bit; legacy markers retain the internal scan path. The
|
||||
bootloader pre-hashes a full payload before invalidating the app, or applies an in-place delta with the
|
||||
complete internal application region as workspace. Companion builds never enable this raw store: some use
|
||||
QSPI as a filesystem, while others simply leave that chip outside OTA ownership. See
|
||||
[the nRF52 QSPI guide](ota_nrf52_qspi.md).
|
||||
|
||||
The signature proves author authenticity; `approval` proves local owner consent - both required to apply.
|
||||
A signature, when present, proves author authenticity and must pass the device allowlist. Unsigned packages
|
||||
remain installable when local policy permits them. The one-shot `approval` marker records local consent for
|
||||
either form before the bootloader may apply it.
|
||||
|
||||
> **Bootloader testing note:** always test apply with a *real different* image (base != target). A same-image
|
||||
> (X->X) "delta" trivially reproduces the target and gives a false positive.
|
||||
|
||||
+45
-18
@@ -4,9 +4,12 @@ This guide is for **node operators**: how to update your MeshCore device's firmw
|
||||
plain language. No cables, no programmer - your node can download a new firmware from a neighbour and
|
||||
install it. (For the technical wire format, see [the OTA protocol spec](ota_protocol.md).)
|
||||
|
||||
LoRa OTA download and installation are present only in supported Keymind destination artifacts whose filename
|
||||
contains `-ota-`; the receiver must already be running one of those install-capable builds. A source can be an
|
||||
OTA-enabled infrastructure node or a source-only Full Companion backed by `motatool`. Intermediate repeaters
|
||||
LoRa OTA download and installation are present only in supported Keymind destination artifacts; the receiver
|
||||
must already be running one of those install-capable builds. Some internal-staging nRF52 targets use a lean
|
||||
`lora_ota_no_external_sensors` target, while matched external-QSPI boards can retain their normal full-sensor
|
||||
repeater features. Release filenames include an OTA marker, but capability must still be confirmed on the
|
||||
running device. A source can be an OTA-enabled infrastructure node or a source-only Full Companion backed
|
||||
by `motatool`. Intermediate repeaters
|
||||
do not need OTA-enabled firmware: current repeater builds transport OTA floods opaquely, subject to their normal
|
||||
forwarding filters, duplicate checks, and flood limits. OTA radio traffic is accepted, generated, and relayed
|
||||
only while `tempradio` is actually running on that node. Every source, receiver, and intermediate repeater must
|
||||
@@ -21,23 +24,45 @@ tempradio 909.950,250,5,5,120
|
||||
|
||||
Use the node's current permitted regional frequency in place of `909.950` when necessary.
|
||||
|
||||
> **Can my node install the update?** Choose a supported repeater artifact carrying the `-ota-` filename stamp.
|
||||
> **Can my node install the update?** Choose a release-table artifact explicitly labelled LoRa-OTA capable,
|
||||
> then confirm `ota self` and `ota status` expose install support; do not infer support from the filename alone.
|
||||
> LoRa OTA firmware is available for supported **ESP32** boards and nRF52 repeater targets. Every nRF52
|
||||
> installation also requires the OTAFIX bootloader built for that exact board; having an OTA-capable
|
||||
> application image alone is not enough. An intermediate repeater only relays packets and needs neither the
|
||||
> `-ota-` image nor OTAFIX. Check the bootloader release for an exact board match before attempting an update.
|
||||
> application image alone is not enough. An intermediate repeater only relays packets and needs neither an
|
||||
> install-capable image nor OTAFIX. Check the bootloader release for an exact board match before attempting an update.
|
||||
|
||||
The following nRF52 repeater targets gained firmware-side LoRa OTA support in this release without losing
|
||||
their normal external-sensor support:
|
||||
The following nRF52 repeater families gained firmware-side LoRa OTA targets in
|
||||
this release. Their ordinary repeater remains the full-sensor build; the
|
||||
install-capable `lora_ota_no_external_sensors` sibling is smaller:
|
||||
|
||||
- Heltec Mesh Solar, T1, and Tower V2
|
||||
- Keepteen LT1, LilyGo T-Impulse Plus, Mesh Pocket, and Nano G2 Ultra
|
||||
- Minewsemi ME25LS01, RAK3401, SenseCAP Solar, and Wio WM1110
|
||||
|
||||
The full-sensor `RAK_4631_repeater` image is too large for the safe nRF52 in-place update limit. Use
|
||||
`RAK_4631_repeater_lora_ota_no_external_sensors` when LoRa OTA is required. That target removes optional
|
||||
external environmental/GPS sensor packages, but retains the RAK4631's built-in battery-voltage reading,
|
||||
battery telemetry, and `battery.alert` behavior.
|
||||
RAK3401 is an important GPS exception: its
|
||||
`RAK_3401_repeater_lora_ota_no_external_sensors` image compiles out GPS support
|
||||
as well as the optional environmental sensors. It will not detect or use a
|
||||
RAK12501. For RAK12501 GPS, use the ordinary full-sensor
|
||||
`RAK_3401_repeater` build and install the GPS in sensor slot A. Slot D's GPS
|
||||
reset/PPS lines conflict with the RAK13302 radio's BUSY/DIO1 lines. The
|
||||
full-sensor RAK3401 build is not the self-updating target described by the
|
||||
RAK3401 compact OTA chain.
|
||||
|
||||
Selected nRF52 repeaters with dedicated external QSPI can now stage the
|
||||
complete package off-chip, so their normal full-sensor repeater build can
|
||||
install a full image or an in-place delta. The current matched families are
|
||||
XIAO nRF52840 and its XIAO-module derivatives, original LilyGo T-Echo,
|
||||
ThinkNode M1/M6, Wio Tracker L1, SenseCAP Solar, and the dedicated RAK4631 +
|
||||
RAK15001 slot-C target. These require the corresponding QSPI-aware OTAFIX bootloader; see
|
||||
[the nRF52 QSPI guide](ota_nrf52_qspi.md).
|
||||
|
||||
The ordinary full-sensor `RAK_4631_repeater` image remains too large for the
|
||||
safe internal in-place update limit. Without external flash, use
|
||||
`RAK_4631_repeater_lora_ota_no_external_sensors`; it removes optional external
|
||||
environmental/GPS packages but retains battery monitoring. A RAK4631 fitted
|
||||
with RAK15001 in sensor slot C can instead use
|
||||
`RAK_4631_repeater_rak15001_slot_c_lora_ota` to retain the full sensor/GPS set
|
||||
and stage full images or deltas off-chip.
|
||||
|
||||
---
|
||||
|
||||
@@ -112,7 +137,8 @@ long ago it was seen. The fit marker:
|
||||
- **[same target]** - the advertised target ID matches this hardware-and-role build. Download and apply
|
||||
still enforce codec, bootloader, signed hardware tag, base hash, and integrity checks.
|
||||
- **[unsupported]** - the target may match, but this build or its bootloader cannot apply that codec. A common
|
||||
example is the source node's self-served **full** image on a single-slot nRF52, which needs an in-place delta.
|
||||
example is the source node's self-served **full** image on an internal-staging nRF52, which needs an
|
||||
in-place delta. A matched external-QSPI nRF52 can accept that full codec.
|
||||
- **[rescue]** - an installable in-place nRF52 delta for the same target, but this running firmware has no
|
||||
valid app-side EndF. It requires the explicit rescue download and install flow below.
|
||||
- **[name]** - a different known board or role (for example `[ProMicro_companion_radio_usb]`). Don't install it.
|
||||
@@ -184,9 +210,9 @@ ota install
|
||||
```
|
||||
|
||||
The node verifies the firmware one last time, and if everything checks out it installs it and **reboots
|
||||
into the new version**. If the check fails, it tells you why and does **not** install. (If you haven't
|
||||
added the signer's key, an unsigned/untrusted image will only install with this explicit command - never
|
||||
automatically.)
|
||||
into the new version**. If the check fails, it tells you why and does **not** install. Unsigned images
|
||||
install only through this explicit command. A signed image whose signer is not in the device allowlist is
|
||||
rejected; trusted signed images can auto-install only when that policy is enabled.
|
||||
|
||||
After it reboots, run `ota status` to confirm the new version.
|
||||
|
||||
@@ -263,8 +289,9 @@ ota key list # show trusted signers
|
||||
ota key rm <public-key-hex> # stop trusting one
|
||||
```
|
||||
|
||||
Only updates signed by a trusted key are eligible for auto-install. Manual `ota install` still lets you
|
||||
install anything yourself, on your own responsibility.
|
||||
Only updates signed by a trusted key are eligible for auto-install. Manual `ota install` permits an unsigned
|
||||
package after all integrity, hardware, base, and bootloader checks pass. A signed package whose signer is not
|
||||
in the device allowlist is rejected rather than silently treated as unsigned.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -34,6 +34,16 @@ target:
|
||||
- endpoint packed version: `0x01110102` (EndF tools render this as `1.17.1.2`)
|
||||
- deployment target key: `63d8df6387eaffd2e25db7d2a8ad967a65202182a48d681d7e7a9260f917280d`
|
||||
|
||||
### GPS limitation
|
||||
|
||||
This chain's exact `RAK_3401_repeater_lora_ota_no_external_sensors` endpoint
|
||||
compiles with `ENV_INCLUDE_GPS` removed. It will not detect, configure, or read
|
||||
a RAK12501 GPS module. GPS requires the ordinary full-sensor
|
||||
`RAK_3401_repeater` firmware and the RAK12501 must be installed in sensor slot
|
||||
A. Do not use slot D with the RAK13302 1 W radio: the GPS reset/PPS signals
|
||||
would overlap the radio's BUSY/DIO1 signals. The full-sensor build is a
|
||||
different target and is not an endpoint of this compact OTA chain.
|
||||
|
||||
Do not use the chain on another target ID, hardware family, starting image, or
|
||||
firmware body hash. The runner checks all four.
|
||||
|
||||
|
||||
@@ -151,6 +151,32 @@ build_src_filter = ${nrf52_base.build_src_filter}
|
||||
+<helpers/ota/*.cpp>
|
||||
lib_deps = ${nrf52_base.lib_deps}
|
||||
|
||||
; Opt-in for repeater environments whose exact board has dedicated QSPI flash
|
||||
; and a matching QSPI-capable OTAFIX bootloader. The raw OTA store owns that
|
||||
; chip, so companion builds that mount QSPI as LittleFS must not use this flag.
|
||||
[nrf52_qspi_ota]
|
||||
build_flags =
|
||||
-D OTA_QSPI_STORE=1
|
||||
|
||||
; RAK15001 is a single-data-line GD25Q16C connected to the WisBlock sensor
|
||||
; SPI bus. The nRF QSPI peripheral can issue its FAST_READ and page-program
|
||||
; commands in single-line mode, but the module's 15 MHz limit requires an
|
||||
; 8 MHz clock. Exact JEDEC matching makes a missing or different shared-bus
|
||||
; device fail closed instead of being mistaken for an OTA store.
|
||||
[nrf52_rak15001_ota]
|
||||
build_flags =
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D OTA_QSPI_SHARED_WISBLOCK_SPI=1
|
||||
-D OTA_QSPI_SCK_ARDUINO_PIN=3
|
||||
-D OTA_QSPI_CS_ARDUINO_PIN=26
|
||||
-D OTA_QSPI_IO0_ARDUINO_PIN=30
|
||||
-D OTA_QSPI_IO1_ARDUINO_PIN=29
|
||||
-D OTA_QSPI_IO2_NOT_CONNECTED=1
|
||||
-D OTA_QSPI_IO3_NOT_CONNECTED=1
|
||||
-D OTA_QSPI_SCK_FREQUENCY=NRF_QSPI_FREQ_32MDIV4
|
||||
-D OTA_QSPI_EXPECTED_JEDEC_ID=0xC84015UL
|
||||
-D OTA_QSPI_EXPECTED_SIZE=2097152UL
|
||||
|
||||
; Compatibility alias retained for existing nRF52840 board definitions.
|
||||
[rak4631_hw]
|
||||
extends = nrf52_lora_ota
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
#if defined(OTA_SD_STORE)
|
||||
#include "OtaStoreSdNrf52.h"
|
||||
#endif
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
#include "OtaStoreQspiNrf52.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
@@ -442,7 +445,9 @@ bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, Ap
|
||||
|
||||
void ota_reboot_to_apply() { // public: set the apply magic + reset (does not return)
|
||||
uint8_t stage_handoff = GPREGRET2_OTA_STAGE_LEGACY;
|
||||
#if defined(OTA_FLASH_STORE)
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
stage_handoff = GPREGRET2_OTA_STAGE_QSPI;
|
||||
#elif defined(OTA_FLASH_STORE)
|
||||
if (ota_nrf52_effective_stage_ceiling() == MOTA_NRF52_STAGE_CEILING_EXPANDED)
|
||||
stage_handoff = GPREGRET2_OTA_STAGE_EXPANDED;
|
||||
#endif
|
||||
@@ -589,28 +594,33 @@ bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowli
|
||||
return ota_apply_mota_nrf52_impl(buf, len, allow, operator_base_hash, local_target_id, st, msg);
|
||||
}
|
||||
|
||||
#if defined(OTA_SD_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
#if defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)
|
||||
static const size_t NRF52_APPLY_MSG_CAP = 96;
|
||||
|
||||
template <typename Store>
|
||||
static bool ota_apply_mota_nrf52_external(Store& store, const SignerAllowlist& allow,
|
||||
uint8_t storage_flag, const char* storage_name,
|
||||
ApplyState& st, char* msg) {
|
||||
st = ApplyState();
|
||||
uint8_t hdr[8], manifest[MOTA_MFL];
|
||||
uint32_t total = store.staged_size();
|
||||
if (total < 8 + MOTA_MFL + 5 || !store.read(0, hdr, sizeof(hdr)) ||
|
||||
memcmp(hdr, MOTA_MAGIC, 4) != 0 || rd_u32le(hdr + 4) != total ||
|
||||
!store.read(8, manifest, sizeof(manifest))) {
|
||||
strcpy(msg, "SD container parse failed");
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s container parse failed", storage_name);
|
||||
return false;
|
||||
}
|
||||
MotaManifest m;
|
||||
if (!mota_parse_manifest(manifest, sizeof(manifest), m)) {
|
||||
strcpy(msg, "SD manifest parse failed");
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s manifest parse failed", storage_name);
|
||||
return false;
|
||||
}
|
||||
const bool full = m.is_full() && m.codec_id == CODEC_FULL &&
|
||||
m.payload_size == m.image_size;
|
||||
const bool delta = !m.is_full() && m.codec_id == CODEC_DETOOLS_INPLACE;
|
||||
if (!full && !delta) {
|
||||
strcpy(msg, "nRF52 SD bootloader accepts full or in-place delta only");
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP,
|
||||
"nRF52 %s bootloader accepts full or in-place delta only", storage_name);
|
||||
return false;
|
||||
}
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
@@ -624,14 +634,15 @@ bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
st.manifest_ok = true;
|
||||
|
||||
OtaBlCaps bl = ota_bootloader_caps();
|
||||
if (!bl.present || !(bl.storage_flags & OTA_BL_STORAGE_SD)) {
|
||||
strcpy(msg, "this bootloader has no SD OTA support - update the bootloader first");
|
||||
if (!bl.present || !(bl.storage_flags & storage_flag)) {
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP,
|
||||
"this bootloader has no %s OTA support - update the bootloader first", storage_name);
|
||||
return false;
|
||||
}
|
||||
if (bl.apply_abi < m.format_ver || !(bl.codec_mask & (1u << m.codec_id))) {
|
||||
snprintf(msg, 159,
|
||||
"bootloader cannot apply this SD update (abi=%u codecs=0x%x; need fmt=%u codec=%u)",
|
||||
bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP,
|
||||
"bootloader cannot apply this %s update (abi=%u codecs=0x%x; need fmt=%u codec=%u)",
|
||||
storage_name, bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -639,12 +650,13 @@ bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
st.sig_ok = vr.sig_ok;
|
||||
st.trusted = vr.trusted;
|
||||
if (!vr.root_ok || !vr.payload_ok || !vr.image_ok) {
|
||||
strcpy(msg, "payload hash mismatch (incomplete or corrupt SD .mota)");
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP,
|
||||
"payload hash mismatch (incomplete or corrupt %s .mota)", storage_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
SelfFwInfo fi;
|
||||
if (delta) {
|
||||
SelfFwInfo fi;
|
||||
if (!ota_self_firmware(fi) || !fi.valid) {
|
||||
strcpy(msg, "cannot read running firmware (no EndF)");
|
||||
return false;
|
||||
@@ -659,16 +671,51 @@ bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
if (!vr.sig_ok) { strcpy(msg, "bad signature"); return false; }
|
||||
if (!vr.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
|
||||
}
|
||||
if (delta) {
|
||||
const uint64_t payload_off64 = 8u + MOTA_MFL + (uint64_t)m.block_count * 4u;
|
||||
uint8_t patch_header[32]; // fixed byte + five detools varints (at most 26 bytes for uint32)
|
||||
uint32_t header_len = m.payload_size < sizeof(patch_header) ? m.payload_size : sizeof(patch_header);
|
||||
InplacePatchDims d;
|
||||
if (payload_off64 > UINT32_MAX || payload_off64 + m.payload_size + 5u != total ||
|
||||
!store.read((uint32_t)payload_off64, patch_header, header_len) ||
|
||||
!parse_inplace_patch_dims(patch_header, header_len, d)) {
|
||||
strcpy(msg, "bad in-place patch header");
|
||||
return false;
|
||||
}
|
||||
if (!mota_nrf52_external_patch_geometry_valid(
|
||||
d.memory, d.segment, d.shift, d.from, d.to,
|
||||
MOTA_NRF52_APP_END - app_base, fi.image_len, m.image_size)) {
|
||||
strcpy(msg, "invalid in-place patch geometry");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!store.approve_for_bootloader()) {
|
||||
snprintf(msg, 159, "SD handoff failed: %s", store.last_error());
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s handoff failed: %s", storage_name,
|
||||
store.last_error());
|
||||
return false;
|
||||
}
|
||||
sprintf(msg, "verified%s %s image on SD; rebooting into bootloader once this reply is sent",
|
||||
vr.is_signed ? " (signer trusted)" : " (unsigned)", full ? "full" : "delta");
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP,
|
||||
"verified%s %s image on %s; rebooting into bootloader once this reply is sent",
|
||||
vr.is_signed ? " (signer trusted)" : " (unsigned)", full ? "full" : "delta",
|
||||
storage_name);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(OTA_SD_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_SD, "SD", st, msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", st, msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#else // native / other platforms
|
||||
|
||||
bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; }
|
||||
|
||||
@@ -86,6 +86,11 @@ class OtaStoreSdNrf52;
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store,
|
||||
const SignerAllowlist& allow, ApplyState& st, char* msg);
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
class OtaStoreQspiNrf52;
|
||||
bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store,
|
||||
const SignerAllowlist& allow, ApplyState& st, char* msg);
|
||||
#endif
|
||||
|
||||
// Commit the (already approved/armed) update and reboot into it - does NOT return. Call this only after
|
||||
// a successful ota_apply_* AND after the confirmation reply has been delivered, so the operator knows
|
||||
|
||||
@@ -30,6 +30,7 @@ struct OtaBlCaps {
|
||||
|
||||
static const uint8_t OTA_BL_STORAGE_SD = 0x01;
|
||||
static const uint8_t OTA_BL_STORAGE_STAGE_CEILING = 0x02;
|
||||
static const uint8_t OTA_BL_STORAGE_QSPI = 0x04;
|
||||
|
||||
// Scan the bootloader flash region for the marker. Returns {present=false} if not found / non-nRF52.
|
||||
inline OtaBlCaps ota_bootloader_caps() {
|
||||
|
||||
+35
-10
@@ -146,7 +146,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
snprintf(reply, 160,
|
||||
"OTA seeder: status | stats | ls=find images | get <id> folder=capture | cancel | "
|
||||
"announce | folder | config. LoRa install is disabled.");
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE)
|
||||
strcpy(reply,
|
||||
"OTA: status | stats | ls | get <id> flash [rescue] | install | rescue install <hash16> | "
|
||||
"cancel | announce | self | folder | config | key");
|
||||
@@ -194,7 +195,10 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
// nRF52 applies via the bootloader - show (cached) whether it can, so `ota get`/`install` won't surprise.
|
||||
// blrc = the bootloader's last apply code (diagnostic; 0xB8=success, see ota_delta.c).
|
||||
const OtaBlCaps& bl = c.bootloaderCaps();
|
||||
#if defined(OTA_SD_STORE)
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
const char* bl_state = !bl.present ? "NONE" :
|
||||
(bl.storage_flags & OTA_BL_STORAGE_QSPI) ? "QSPI" : "NO-QSPI";
|
||||
#elif defined(OTA_SD_STORE)
|
||||
const char* bl_state = !bl.present ? "NONE" :
|
||||
(bl.storage_flags & OTA_BL_STORAGE_SD) ? "SD" : "NO-SD";
|
||||
#else
|
||||
@@ -269,7 +273,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
#if defined(NRF52_PLATFORM)
|
||||
const OtaBlCaps& list_bl = c.bootloaderCaps();
|
||||
#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_QSPI_STORE)
|
||||
SelfFwInfo list_self;
|
||||
bool list_has_endf = ota_self_firmware(list_self) && list_self.valid;
|
||||
#endif
|
||||
@@ -302,10 +307,13 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
&& h->codec < 16 && (list_bl.codec_mask & (1u << h->codec));
|
||||
#if defined(OTA_SD_STORE)
|
||||
installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_SD);
|
||||
#elif defined(OTA_QSPI_STORE)
|
||||
installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_QSPI);
|
||||
#endif
|
||||
#endif
|
||||
if (!installable) fit = "unsupported";
|
||||
#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_QSPI_STORE)
|
||||
else if (!list_has_endf) fit = "rescue";
|
||||
#endif
|
||||
else fit = "same target";
|
||||
@@ -428,9 +436,15 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
strcpy(reply, "ERR bootloader cannot apply an update staged on SD; update it over USB first");
|
||||
return true;
|
||||
}
|
||||
#elif defined(OTA_QSPI_STORE)
|
||||
if (!(bl.storage_flags & OTA_BL_STORAGE_QSPI)) {
|
||||
strcpy(reply, "ERR bootloader cannot apply an update staged on QSPI; update it over USB first");
|
||||
return true;
|
||||
}
|
||||
#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_QSPI_STORE)
|
||||
SelfFwInfo self;
|
||||
bool has_endf = ota_self_firmware(self) && self.valid;
|
||||
if (!has_endf && !rescue) {
|
||||
@@ -489,7 +503,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
c.manager.reset_session(); c.manager.want(0); c.manager.want_mid(nullptr);
|
||||
c.fetch_to_folder = false;
|
||||
c.manager.set_fetch_store(&c.fetch_store); // revert to the default flash store (a folder pull switched it)
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE) && !defined(OTA_QSPI_STORE)
|
||||
c.manager.set_accept_full(false);
|
||||
#endif
|
||||
c.fetch_store.clear(); c.serving = false; c.serve_expected = 0; c.session_started_ms = 0;
|
||||
@@ -517,7 +531,17 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// nRF52 applies via the bootloader, so surface whether THIS device's bootloader can install this store.
|
||||
const OtaBlCaps& bl = c.bootloaderCaps(); // cached (flash scanned once)
|
||||
#if defined(OTA_SD_STORE)
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
uint32_t qspi_capacity = c.fetch_store.capacity();
|
||||
n += snprintf(reply + n, 160 - n, " | QSPI store:%s%uK",
|
||||
qspi_capacity ? "" : "ERR ", (unsigned)(qspi_capacity / 1024));
|
||||
if (bl.present && (bl.storage_flags & OTA_BL_STORAGE_QSPI))
|
||||
snprintf(reply + n, 160 - n, " | bootloader: QSPI apply OK (abi=%u codecs=0x%x)",
|
||||
bl.apply_abi, bl.codec_mask);
|
||||
else
|
||||
snprintf(reply + n, 160 - n,
|
||||
" | bootloader: NO QSPI mota-apply support (install will refuse)");
|
||||
#elif defined(OTA_SD_STORE)
|
||||
if (bl.present && (bl.storage_flags & OTA_BL_STORAGE_SD))
|
||||
snprintf(reply + n, 160 - n, " | bootloader: SD apply OK (abi=%u codecs=0x%x)", bl.apply_abi, bl.codec_mask);
|
||||
else
|
||||
@@ -538,7 +562,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
// deliberately not an alias or automatic fallback: the operator must name `install` and provide
|
||||
// the exact 8-byte base hash carried by the already-fetched package. The bootloader independently
|
||||
// hashes the running app and refuses a mismatch before writing any application flash.
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && !defined(OTA_SEEDER_ONLY)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE) && !defined(OTA_SEEDER_ONLY)
|
||||
if (c.fetch_to_folder) {
|
||||
strcpy(reply, "ERR the complete update was captured to a folder, not staged for install; use `ota cancel`");
|
||||
return true;
|
||||
@@ -827,7 +852,7 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
strcpy(reply, "ERR completed fetch is in the host folder, not local verification storage");
|
||||
return true;
|
||||
}
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
if (c.manager.fetchState() == OtaManager::COMPLETE) {
|
||||
VerifyResult r = ota_verify(static_cast<const OtaStore&>(c.fetch_store), c.allow);
|
||||
sprintf(reply, "verify parsed=%d root=%d payload=%d img=%d signed=%d sig=%d trust=%d | ok=%d auto=%d",
|
||||
@@ -837,7 +862,7 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
}
|
||||
#endif
|
||||
const uint8_t* buf; uint32_t len;
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
buf = c.serve_buf; len = c.serve_buf ? c.serve_expected : 0;
|
||||
#else
|
||||
if (c.manager.fetchState() == OtaManager::COMPLETE) { buf = c.fetch_store.data(); len = c.fetch_store.staged_size(); }
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#include "OtaFormat.h"
|
||||
#include "OtaSelf.h" // ota_self_firmware() - prefer self-describing EndF identity at begin()
|
||||
#include "OtaBlInfo.h" // bootloader OTA-apply capability marker (nRF52); cached after first read
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
#include "OtaStoreQspiNrf52.h"
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#include "OtaStoreSdNrf52.h"
|
||||
#include "OtaCacheSdNrf52.h"
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
@@ -31,12 +33,11 @@
|
||||
#endif
|
||||
|
||||
// Per-device OTA singleton shared by the CLI (OtaCli) and the mesh adapter (the example's MyMesh).
|
||||
// Holds the session engine, a staging store (fetch), a RAM serve buffer, and the signer allowlist.
|
||||
// nRF52 stages into FLASH (OtaStoreFlashNrf52): a delta can be 100 KB+, too big to hold in RAM, and the
|
||||
// COMPLETE container must persist so the bootloader can apply it after reboot. A flash page-erase halts
|
||||
// the CPU (~85 ms) and starves the LoRa RX, so the store COALESCES writes to the 4 KB page (the erase
|
||||
// unit) and commits each page once, off the per-packet path (see OtaManager.h) - RAM stays O(one page).
|
||||
// (v1 has no mid-transfer resume; an interrupted fetch simply restarts.) ESP32/native use the RAM store.
|
||||
// Holds the session engine, a persistent staging store (fetch), a RAM serve buffer, and the signer
|
||||
// allowlist. nRF52 uses one of three stores selected by the exact target: internal flash for in-place
|
||||
// deltas, raw microSD, or dedicated raw QSPI for full images and in-place deltas. Flash-backed stores
|
||||
// coalesce writes at their erase-page boundary and checkpoint payload before leaf metadata, so an
|
||||
// interrupted fetch can be verified and resumed without trusting stale progress markers.
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
@@ -50,7 +51,8 @@ class FolderMotaStore; // pull destination over the seeder link (full type onl
|
||||
#define OTA_SERVE_BUF_SIZE 1
|
||||
// nRF52 self-serving streams from flash; this buffer is only for the manual `ota dev stage` helper.
|
||||
// Keep it to one flash page so the OTA singleton does not consume another 16 KB of scarce SRAM.
|
||||
#elif defined(NRF52_PLATFORM) && (defined(OTA_FLASH_STORE) || defined(OTA_SD_STORE))
|
||||
#elif defined(NRF52_PLATFORM) && \
|
||||
(defined(OTA_FLASH_STORE) || defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
#define OTA_SERVE_BUF_SIZE 4096
|
||||
#else
|
||||
#define OTA_SERVE_BUF_SIZE 16384
|
||||
@@ -67,6 +69,8 @@ struct OtaContext {
|
||||
// store object for OtaManager, while folder captures replace it with the
|
||||
// host-backed FolderMotaStore for the duration of the pull.
|
||||
OtaStoreRam<1> fetch_store;
|
||||
#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)
|
||||
OtaStoreSdNrf52 fetch_store; // MeshTower V2: persistent SD staging, full + delta
|
||||
OtaCacheSdNrf52 sd_cache; // persistent capture + source for every OTA container heard
|
||||
@@ -161,7 +165,7 @@ struct OtaContext {
|
||||
msg[95] = 0;
|
||||
return false;
|
||||
#else
|
||||
#if !defined(NRF52_PLATFORM) || defined(OTA_SD_STORE)
|
||||
#if !defined(NRF52_PLATFORM) || defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)
|
||||
if (rescue_base_hash) {
|
||||
strncpy(msg, "rescue is only for internal-flash nRF52 builds", 96);
|
||||
msg[95] = 0;
|
||||
@@ -191,7 +195,7 @@ struct OtaContext {
|
||||
}
|
||||
}
|
||||
bool ok;
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
ok = ota_apply_mota_nrf52(fetch_store, allow, apply_st, msg);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
if (rescue_base_hash) {
|
||||
@@ -446,7 +450,7 @@ struct OtaContext {
|
||||
manager.set_accept_full(true);
|
||||
manager.set_autofetch(OtaManager::AUTOFETCH_OFF);
|
||||
autoinstall = AUTOINSTALL_OFF;
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#elif defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
manager.set_accept_full(true);
|
||||
manager.set_apply_codec(CODEC_DETOOLS_INPLACE);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
|
||||
@@ -37,6 +37,7 @@ static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096u;
|
||||
static const uint8_t GPREGRET_OTA_APPLY = 0x6Au; // distinct from DFU magics 0x57/0x4E/0xA8
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_LEGACY = 0xD4u;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_EXPANDED = 0xEDu;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_QSPI = 0x51u;
|
||||
|
||||
// Firmware without a valid EndF and older host tooling fall back to this conservative apply workspace.
|
||||
// New motatool builds read the firmware's appended layout record and derive memory_size from the actual
|
||||
@@ -106,6 +107,20 @@ inline bool mota_nrf52_layout_valid(uint32_t app_base) {
|
||||
return mota_nrf52_layout_valid(app_base, mota_nrf52_layout_stage_ceiling());
|
||||
}
|
||||
|
||||
// Validate the five sizes encoded at the start of a detools in-place patch
|
||||
// before an external SD/QSPI handoff is approved. External media leaves the
|
||||
// complete application region available as workspace; the bootloader repeats
|
||||
// these checks before its first destructive write.
|
||||
inline bool mota_nrf52_external_patch_geometry_valid(uint32_t memory, uint32_t segment,
|
||||
uint32_t shift, uint32_t from,
|
||||
uint32_t to, uint32_t workspace_span,
|
||||
uint32_t running_image_size,
|
||||
uint32_t target_image_size) {
|
||||
return memory != 0 && memory <= workspace_span && segment == MOTA_NRF52_FLASH_PAGE &&
|
||||
shift <= memory && shift % segment == 0 && from <= memory - shift &&
|
||||
from == running_image_size && to == target_image_size && to <= memory;
|
||||
}
|
||||
|
||||
inline uint32_t mota_nrf52_stage_capacity(uint32_t app_base, uint32_t app_end,
|
||||
uint32_t stage_ceiling) {
|
||||
if (!mota_nrf52_layout_valid(app_base, stage_ceiling) || app_end < app_base ||
|
||||
|
||||
@@ -0,0 +1,701 @@
|
||||
#include "OtaStoreQspiNrf52.h"
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
|
||||
#include "OtaByteIO.h"
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
#include "hal/nrf_qspi.h"
|
||||
#include "nrf.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
namespace {
|
||||
|
||||
#if defined(OTA_QSPI_SCK_PHYSICAL_PIN) && defined(OTA_QSPI_SCK_ARDUINO_PIN)
|
||||
#error "QSPI SCK must use either a physical or Arduino pin override"
|
||||
#elif !defined(OTA_QSPI_SCK_PHYSICAL_PIN) && !defined(OTA_QSPI_SCK_ARDUINO_PIN)
|
||||
#define OTA_QSPI_SCK_ARDUINO_PIN PIN_QSPI_SCK
|
||||
#endif
|
||||
#if defined(OTA_QSPI_CS_PHYSICAL_PIN) && defined(OTA_QSPI_CS_ARDUINO_PIN)
|
||||
#error "QSPI CS must use either a physical or Arduino pin override"
|
||||
#elif !defined(OTA_QSPI_CS_PHYSICAL_PIN) && !defined(OTA_QSPI_CS_ARDUINO_PIN)
|
||||
#define OTA_QSPI_CS_ARDUINO_PIN PIN_QSPI_CS
|
||||
#endif
|
||||
#if defined(OTA_QSPI_IO0_PHYSICAL_PIN) && defined(OTA_QSPI_IO0_ARDUINO_PIN)
|
||||
#error "QSPI IO0 must use either a physical or Arduino pin override"
|
||||
#elif !defined(OTA_QSPI_IO0_PHYSICAL_PIN) && !defined(OTA_QSPI_IO0_ARDUINO_PIN)
|
||||
#define OTA_QSPI_IO0_ARDUINO_PIN PIN_QSPI_IO0
|
||||
#endif
|
||||
#if defined(OTA_QSPI_IO1_PHYSICAL_PIN) && defined(OTA_QSPI_IO1_ARDUINO_PIN)
|
||||
#error "QSPI IO1 must use either a physical or Arduino pin override"
|
||||
#elif !defined(OTA_QSPI_IO1_PHYSICAL_PIN) && !defined(OTA_QSPI_IO1_ARDUINO_PIN)
|
||||
#define OTA_QSPI_IO1_ARDUINO_PIN PIN_QSPI_IO1
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_SCK_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_SCK_PHYSICAL_PIN) < 48, "invalid physical QSPI SCK pin");
|
||||
#else
|
||||
static_assert((uint32_t)(OTA_QSPI_SCK_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI SCK pin");
|
||||
#endif
|
||||
#if defined(OTA_QSPI_CS_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_CS_PHYSICAL_PIN) < 48, "invalid physical QSPI CS pin");
|
||||
#else
|
||||
static_assert((uint32_t)(OTA_QSPI_CS_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI CS pin");
|
||||
#endif
|
||||
#if defined(OTA_QSPI_IO0_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO0_PHYSICAL_PIN) < 48, "invalid physical QSPI IO0 pin");
|
||||
#else
|
||||
static_assert((uint32_t)(OTA_QSPI_IO0_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO0 pin");
|
||||
#endif
|
||||
#if defined(OTA_QSPI_IO1_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO1_PHYSICAL_PIN) < 48, "invalid physical QSPI IO1 pin");
|
||||
#else
|
||||
static_assert((uint32_t)(OTA_QSPI_IO1_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO1 pin");
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_IO2_NOT_CONNECTED)
|
||||
#if defined(OTA_QSPI_IO2_ARDUINO_PIN) || defined(OTA_QSPI_IO2_PHYSICAL_PIN)
|
||||
#error "QSPI IO2 cannot be both connected and disconnected"
|
||||
#endif
|
||||
#elif defined(OTA_QSPI_IO2_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO2_PHYSICAL_PIN) < 48, "invalid physical QSPI IO2 pin");
|
||||
#elif defined(OTA_QSPI_IO2_ARDUINO_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO2_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO2 pin");
|
||||
#else
|
||||
#define OTA_QSPI_IO2_ARDUINO_PIN PIN_QSPI_IO2
|
||||
static_assert((uint32_t)(OTA_QSPI_IO2_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO2 pin");
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_IO3_NOT_CONNECTED)
|
||||
#if defined(OTA_QSPI_IO3_ARDUINO_PIN) || defined(OTA_QSPI_IO3_PHYSICAL_PIN)
|
||||
#error "QSPI IO3 cannot be both connected and disconnected"
|
||||
#endif
|
||||
#elif defined(OTA_QSPI_IO3_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO3_PHYSICAL_PIN) < 48, "invalid physical QSPI IO3 pin");
|
||||
#elif defined(OTA_QSPI_IO3_ARDUINO_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO3_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO3 pin");
|
||||
#else
|
||||
#define OTA_QSPI_IO3_ARDUINO_PIN PIN_QSPI_IO3
|
||||
static_assert((uint32_t)(OTA_QSPI_IO3_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO3 pin");
|
||||
#endif
|
||||
|
||||
static uint8_t arduino_to_physical(uint32_t arduino_pin) {
|
||||
return (uint8_t)g_ADigitalPinMap[arduino_pin];
|
||||
}
|
||||
|
||||
static uint8_t qspi_sck_pin() {
|
||||
#ifdef OTA_QSPI_SCK_PHYSICAL_PIN
|
||||
return OTA_QSPI_SCK_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_SCK_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t qspi_cs_pin() {
|
||||
#ifdef OTA_QSPI_CS_PHYSICAL_PIN
|
||||
return OTA_QSPI_CS_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_CS_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t qspi_io0_pin() {
|
||||
#ifdef OTA_QSPI_IO0_PHYSICAL_PIN
|
||||
return OTA_QSPI_IO0_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_IO0_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t qspi_io1_pin() {
|
||||
#ifdef OTA_QSPI_IO1_PHYSICAL_PIN
|
||||
return OTA_QSPI_IO1_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_IO1_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t qspi_io2_pin() {
|
||||
#ifdef OTA_QSPI_IO2_NOT_CONNECTED
|
||||
return NRF_QSPI_PIN_NOT_CONNECTED;
|
||||
#elif defined(OTA_QSPI_IO2_PHYSICAL_PIN)
|
||||
return OTA_QSPI_IO2_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_IO2_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t qspi_io3_pin() {
|
||||
#ifdef OTA_QSPI_IO3_NOT_CONNECTED
|
||||
return NRF_QSPI_PIN_NOT_CONNECTED;
|
||||
#elif defined(OTA_QSPI_IO3_PHYSICAL_PIN)
|
||||
return OTA_QSPI_IO3_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_IO3_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
OtaStoreQspiNrf52::OtaStoreQspiNrf52() {
|
||||
resetSession();
|
||||
}
|
||||
|
||||
OtaStoreQspiNrf52::~OtaStoreQspiNrf52() {
|
||||
releaseFlash();
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::fail(const char *message) {
|
||||
_io_ok = false;
|
||||
strncpy(_error, message ? message : "QSPI error", sizeof(_error) - 1);
|
||||
_error[sizeof(_error) - 1] = 0;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::resetSession() {
|
||||
_total = 0;
|
||||
_io_ok = true;
|
||||
_meta_dirty = false;
|
||||
_data_dirty = false;
|
||||
_data_page_index = INVALID_PAGE;
|
||||
memset(_meta_page, 0xFF, sizeof(_meta_page));
|
||||
memset(_data_page, 0xFF, sizeof(_data_page));
|
||||
memset(_known_pages, 0, sizeof(_known_pages));
|
||||
_error[0] = 0;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::pageKnown(uint32_t page) const {
|
||||
return page < MAX_PAGES && (_known_pages[page >> 3] & (1u << (page & 7))) != 0;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::setPageKnown(uint32_t page) {
|
||||
if (page < MAX_PAGES) _known_pages[page >> 3] |= (uint8_t)(1u << (page & 7));
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::waitReady(uint32_t timeout_ms) {
|
||||
uint32_t started = millis();
|
||||
while (!nrf_qspi_event_check(NRF_QSPI, NRF_QSPI_EVENT_READY)) {
|
||||
if ((uint32_t)(millis() - started) >= timeout_ms) return false;
|
||||
delay(1);
|
||||
}
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::customInstruction(uint8_t opcode, uint8_t length, uint8_t *rx) {
|
||||
nrf_qspi_cinstr_conf_t config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.opcode = opcode;
|
||||
config.length = (nrf_qspi_cinstr_len_t)length;
|
||||
config.io2_level = true;
|
||||
config.io3_level = true;
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_cinstr_transfer_start(NRF_QSPI, &config);
|
||||
if (!waitReady(1000)) return false;
|
||||
if (rx) nrf_qspi_cinstrdata_get(NRF_QSPI, config.length, rx);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::ensureFlash() {
|
||||
if (_qspi_ready) return true;
|
||||
_error[0] = 0;
|
||||
_io_ok = true;
|
||||
|
||||
#if defined(OTA_QSPI_POWER_PIN)
|
||||
pinMode(OTA_QSPI_POWER_PIN, OUTPUT);
|
||||
digitalWrite(OTA_QSPI_POWER_PIN, HIGH);
|
||||
delay(2);
|
||||
#elif defined(PIN_FLASH_EN)
|
||||
pinMode(PIN_FLASH_EN, OUTPUT);
|
||||
digitalWrite(PIN_FLASH_EN, HIGH);
|
||||
delay(2);
|
||||
#elif defined(QSPI_FLASH_EN)
|
||||
pinMode(QSPI_FLASH_EN, OUTPUT);
|
||||
digitalWrite(QSPI_FLASH_EN, HIGH);
|
||||
delay(2);
|
||||
#endif
|
||||
|
||||
NVIC_DisableIRQ(QSPI_IRQn);
|
||||
NVIC_ClearPendingIRQ(QSPI_IRQn);
|
||||
nrf_qspi_int_disable(NRF_QSPI, 0xFFFFFFFFUL);
|
||||
|
||||
nrf_qspi_pins_t pins;
|
||||
pins.sck_pin = qspi_sck_pin();
|
||||
pins.csn_pin = qspi_cs_pin();
|
||||
pins.io0_pin = qspi_io0_pin();
|
||||
pins.io1_pin = qspi_io1_pin();
|
||||
pins.io2_pin = qspi_io2_pin();
|
||||
pins.io3_pin = qspi_io3_pin();
|
||||
nrf_qspi_pins_set(NRF_QSPI, &pins);
|
||||
|
||||
nrf_qspi_prot_conf_t protocol;
|
||||
protocol.readoc = NRF_QSPI_READOC_FASTREAD;
|
||||
protocol.writeoc = NRF_QSPI_WRITEOC_PP;
|
||||
protocol.addrmode = NRF_QSPI_ADDRMODE_24BIT;
|
||||
protocol.dpmconfig = false;
|
||||
nrf_qspi_ifconfig0_set(NRF_QSPI, &protocol);
|
||||
|
||||
nrf_qspi_phy_conf_t physical;
|
||||
physical.sck_delay = 5;
|
||||
physical.dpmen = false;
|
||||
physical.spi_mode = NRF_QSPI_MODE_0;
|
||||
#ifdef OTA_QSPI_SCK_FREQUENCY
|
||||
physical.sck_freq = static_cast<nrf_qspi_frequency_t>(OTA_QSPI_SCK_FREQUENCY);
|
||||
#else
|
||||
physical.sck_freq = NRF_QSPI_FREQ_32MDIV2;
|
||||
#endif
|
||||
nrf_qspi_ifconfig1_set(NRF_QSPI, &physical);
|
||||
|
||||
nrf_qspi_enable(NRF_QSPI);
|
||||
_qspi_active = true;
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_ACTIVATE);
|
||||
if (!waitReady(1000)) {
|
||||
releaseFlash();
|
||||
fail("QSPI activate timed out");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Release from deep power-down, then identify capacity from the JEDEC byte.
|
||||
if (!customInstruction(0xAB, NRF_QSPI_CINSTR_LEN_1B)) {
|
||||
releaseFlash();
|
||||
fail("QSPI wake failed");
|
||||
return false;
|
||||
}
|
||||
_qspi_awake = true;
|
||||
delayMicroseconds(50);
|
||||
alignas(4) uint8_t jedec[4] = { 0, 0, 0, 0 };
|
||||
if (!customInstruction(0x9F, NRF_QSPI_CINSTR_LEN_4B, jedec) || jedec[0] == 0 || jedec[0] == 0xFF ||
|
||||
jedec[2] < 20 || jedec[2] > 24) {
|
||||
releaseFlash();
|
||||
fail("QSPI JEDEC ID/capacity unsupported");
|
||||
return false;
|
||||
}
|
||||
#ifdef OTA_QSPI_EXPECTED_JEDEC_ID
|
||||
const uint32_t jedec_id = ((uint32_t)jedec[0] << 16) | ((uint32_t)jedec[1] << 8) | jedec[2];
|
||||
if (jedec_id != (uint32_t)OTA_QSPI_EXPECTED_JEDEC_ID) {
|
||||
releaseFlash();
|
||||
fail("QSPI JEDEC ID does not match target");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
_flash_size = 1UL << jedec[2];
|
||||
if (_flash_size > MAX_FLASH) {
|
||||
releaseFlash();
|
||||
fail("QSPI flash exceeds 24-bit staging limit");
|
||||
return false;
|
||||
}
|
||||
#ifdef OTA_QSPI_EXPECTED_SIZE
|
||||
if (_flash_size != (uint32_t)OTA_QSPI_EXPECTED_SIZE) {
|
||||
releaseFlash();
|
||||
fail("QSPI capacity does not match target");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
_qspi_ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::releaseFlash() {
|
||||
if (_qspi_awake) {
|
||||
// The repeater can remain idle for hours after a capacity/status probe or
|
||||
// a completed checkpoint. Put the NOR into deep power-down before
|
||||
// releasing the nRF QSPI peripheral instead of leaving both active for
|
||||
// the rest of the boot. ensureFlash() issues 0xAB on the next operation.
|
||||
(void)customInstruction(0xB9, NRF_QSPI_CINSTR_LEN_1B);
|
||||
delayMicroseconds(5);
|
||||
}
|
||||
// Match nrfx_qspi_uninit(): DEACTIVATE does not require a READY wait before
|
||||
// disabling the peripheral. Trigger it after every successful ENABLE, even
|
||||
// when wake or JEDEC identification failed before _qspi_ready was set.
|
||||
if (_qspi_active) {
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_DEACTIVATE);
|
||||
nrf_qspi_disable(NRF_QSPI);
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
}
|
||||
_qspi_active = false;
|
||||
_qspi_awake = false;
|
||||
_qspi_ready = false;
|
||||
#if defined(OTA_QSPI_POWER_PIN)
|
||||
digitalWrite(OTA_QSPI_POWER_PIN, LOW);
|
||||
#elif defined(PIN_FLASH_EN)
|
||||
digitalWrite(PIN_FLASH_EN, LOW);
|
||||
#elif defined(QSPI_FLASH_EN)
|
||||
digitalWrite(QSPI_FLASH_EN, LOW);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::dmaReadAligned(uint32_t address, uint32_t length) {
|
||||
// nRF52840 QSPI EasyDMA requires a word-aligned RAM buffer and a transfer
|
||||
// count that is a multiple of four. Keep the external address aligned too,
|
||||
// so every supported SDK/peripheral revision receives the same safe shape.
|
||||
if (!length || length > sizeof(_bounce) || ((address | length) & 3u) != 0 ||
|
||||
(uint64_t)address + length > _flash_size) {
|
||||
fail("QSPI aligned read invalid");
|
||||
return false;
|
||||
}
|
||||
nrf_qspi_read_buffer_set(NRF_QSPI, _bounce, length, address);
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_READSTART);
|
||||
if (!waitReady(5000)) {
|
||||
fail("QSPI read timed out");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::dmaWriteAligned(uint32_t address, uint32_t length) {
|
||||
if (!length || length > sizeof(_bounce) || ((address | length) & 3u) != 0 ||
|
||||
(address & (PROGRAM - 1)) + length > PROGRAM ||
|
||||
(uint64_t)address + length > _flash_size) {
|
||||
fail("QSPI aligned program invalid");
|
||||
return false;
|
||||
}
|
||||
nrf_qspi_write_buffer_set(NRF_QSPI, _bounce, length, address);
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_WRITESTART);
|
||||
if (!waitReady(10000)) {
|
||||
fail("QSPI program timed out");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::rawRead(uint32_t address, void *data, uint32_t length) {
|
||||
if (length && !data) {
|
||||
fail("QSPI read buffer missing");
|
||||
return false;
|
||||
}
|
||||
if (!length) return true;
|
||||
if (!ensureFlash() || (uint64_t)address + length > _flash_size) {
|
||||
if (_io_ok) fail("QSPI read outside flash");
|
||||
return false;
|
||||
}
|
||||
uint8_t *out = static_cast<uint8_t *>(data);
|
||||
while (length) {
|
||||
const uint32_t aligned_address = address & ~3u;
|
||||
const uint32_t prefix = address - aligned_address;
|
||||
uint32_t n = length;
|
||||
if (n > sizeof(_bounce) - prefix) n = sizeof(_bounce) - prefix;
|
||||
const uint32_t dma_length = (prefix + n + 3u) & ~3u;
|
||||
if (!dmaReadAligned(aligned_address, dma_length)) return false;
|
||||
memcpy(out, _bounce + prefix, n);
|
||||
address += n;
|
||||
out += n;
|
||||
length -= n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::rawWrite(uint32_t address, const void *data, uint32_t length) {
|
||||
if (length && !data) {
|
||||
fail("QSPI program buffer missing");
|
||||
return false;
|
||||
}
|
||||
if (!length) return true;
|
||||
if (!ensureFlash() || (uint64_t)address + length > _flash_size) {
|
||||
if (_io_ok) fail("QSPI program outside flash");
|
||||
return false;
|
||||
}
|
||||
const uint8_t *in = static_cast<const uint8_t *>(data);
|
||||
while (length) {
|
||||
const uint32_t aligned_address = address & ~3u;
|
||||
const uint32_t prefix = address - aligned_address;
|
||||
const uint32_t page_room = PROGRAM - (aligned_address & (PROGRAM - 1));
|
||||
uint32_t n = length;
|
||||
if (n > page_room - prefix) n = page_room - prefix;
|
||||
const uint32_t dma_length = (prefix + n + 3u) & ~3u;
|
||||
|
||||
// Read-modify-program the aligned EasyDMA window. NOR flash cannot change
|
||||
// a programmed zero back to one without erasing the 4 KiB sector, so
|
||||
// reject such a request instead of silently corrupting the container.
|
||||
if (!dmaReadAligned(aligned_address, dma_length)) return false;
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
const uint8_t old_value = _bounce[prefix + i];
|
||||
const uint8_t new_value = in[i];
|
||||
if ((old_value & new_value) != new_value) {
|
||||
fail("QSPI program requires erase");
|
||||
return false;
|
||||
}
|
||||
_bounce[prefix + i] = new_value;
|
||||
}
|
||||
if (!dmaWriteAligned(aligned_address, dma_length)) return false;
|
||||
address += n;
|
||||
in += n;
|
||||
length -= n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::rawErasePage(uint32_t address) {
|
||||
if (!ensureFlash() || (address & (PAGE - 1)) != 0 || address > _flash_size - PAGE) return false;
|
||||
nrf_qspi_erase_ptr_set(NRF_QSPI, address, NRF_QSPI_ERASE_LEN_4KB);
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_ERASESTART);
|
||||
if (!waitReady(30000)) {
|
||||
fail("QSPI erase timed out");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::flushPage(uint32_t page, const uint8_t *data) {
|
||||
if (!_io_ok || page >= MAX_PAGES || (uint64_t)(page + 1) * PAGE > _flash_size ||
|
||||
!rawErasePage(page * PAGE) || !rawWrite(page * PAGE, data, PAGE)) {
|
||||
if (_io_ok) fail("QSPI page write failed");
|
||||
return false;
|
||||
}
|
||||
alignas(4) uint8_t verify[PROGRAM];
|
||||
for (uint32_t off = 0; off < PAGE; off += sizeof(verify)) {
|
||||
if (!rawRead(page * PAGE + off, verify, sizeof(verify)) ||
|
||||
memcmp(verify, data + off, sizeof(verify)) != 0) {
|
||||
fail("QSPI page verify failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setPageKnown(page);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::flushMeta() {
|
||||
if (!_meta_dirty) return _io_ok;
|
||||
if (!flushPage(0, _meta_page)) return false;
|
||||
_meta_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::flushData() {
|
||||
if (_data_page_index == INVALID_PAGE || !_data_dirty) return _io_ok;
|
||||
if (!flushPage(_data_page_index, _data_page)) return false;
|
||||
_data_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::useDataPage(uint32_t page) {
|
||||
if (page == 0 || page >= MAX_PAGES) return false;
|
||||
if (_data_page_index == page) return true;
|
||||
if (!flushData()) return false;
|
||||
if (pageKnown(page)) {
|
||||
if (!rawRead(page * PAGE, _data_page, PAGE)) {
|
||||
fail("QSPI page load failed");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
memset(_data_page, 0xFF, PAGE);
|
||||
setPageKnown(page);
|
||||
}
|
||||
_data_page_index = page;
|
||||
_data_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t OtaStoreQspiNrf52::capacity() const {
|
||||
OtaStoreQspiNrf52 *self = const_cast<OtaStoreQspiNrf52 *>(this);
|
||||
uint32_t result = self->ensureFlash() ? self->_flash_size : 0;
|
||||
self->releaseFlash();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::plan_layout(bool, uint32_t image_size, uint32_t, uint32_t payload_size) {
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (image_size == 0 || payload_size == 0 || app_base >= MOTA_NRF52_APP_END ||
|
||||
image_size > MOTA_NRF52_APP_END - app_base) {
|
||||
fail("image exceeds nRF52 application region");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
bool result = ensureFlash();
|
||||
releaseFlash();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::begin(uint32_t total_size) {
|
||||
if (!ensureFlash() || total_size < 13 || total_size > _flash_size) {
|
||||
if (_io_ok) fail("QSPI lacks space for update");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Immediately invalidate an older raw container. Page zero is erased and
|
||||
// replaced with the new header at the first checkpoint/finalize.
|
||||
uint8_t zero[4] = { 0, 0, 0, 0 };
|
||||
uint8_t check[sizeof(zero)];
|
||||
if (!rawWrite(0, zero, sizeof(zero)) || !rawRead(0, check, sizeof(check)) ||
|
||||
memcmp(check, zero, sizeof(zero)) != 0) {
|
||||
fail("QSPI old-container invalidation failed");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
resetSession();
|
||||
_total = total_size;
|
||||
releaseFlash();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::set_meta_size(uint32_t meta_bytes) {
|
||||
return _total >= 13 && meta_bytes <= PAGE;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::write(uint32_t offset, const uint8_t *data, uint32_t len) {
|
||||
if (!_io_ok || !data || (uint64_t)offset + len > _total) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
while (len) {
|
||||
uint32_t page = offset / PAGE;
|
||||
uint32_t in_page = offset & (PAGE - 1);
|
||||
uint32_t n = PAGE - in_page;
|
||||
if (n > len) n = len;
|
||||
if (page == 0) {
|
||||
memcpy(_meta_page + in_page, data, n);
|
||||
_meta_dirty = true;
|
||||
} else {
|
||||
if (!useDataPage(page)) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
memcpy(_data_page + in_page, data, n);
|
||||
_data_dirty = true;
|
||||
}
|
||||
offset += n;
|
||||
data += n;
|
||||
len -= n;
|
||||
}
|
||||
releaseFlash();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::read(uint32_t offset, uint8_t *buf, uint32_t len) const {
|
||||
OtaStoreQspiNrf52 *self = const_cast<OtaStoreQspiNrf52 *>(this);
|
||||
if (!_io_ok || !buf || (uint64_t)offset + len > _total) {
|
||||
self->releaseFlash();
|
||||
return false;
|
||||
}
|
||||
while (len) {
|
||||
uint32_t page = offset / PAGE;
|
||||
uint32_t in_page = offset & (PAGE - 1);
|
||||
uint32_t n = PAGE - in_page;
|
||||
if (n > len) n = len;
|
||||
if (page == 0) {
|
||||
memcpy(buf, _meta_page + in_page, n);
|
||||
} else if (page == _data_page_index) {
|
||||
memcpy(buf, _data_page + in_page, n);
|
||||
} else if (pageKnown(page)) {
|
||||
if (!self->rawRead(offset, buf, n)) {
|
||||
self->releaseFlash();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
memset(buf, 0xFF, n);
|
||||
}
|
||||
offset += n;
|
||||
buf += n;
|
||||
len -= n;
|
||||
}
|
||||
self->releaseFlash();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::finalize() {
|
||||
if (!_total || !_io_ok) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
// Persist payload first, then the leaf-progress metadata that declares it.
|
||||
bool result = flushData() && flushMeta();
|
||||
releaseFlash();
|
||||
return result;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::checkpoint() {
|
||||
if (!_total || !_io_ok) {
|
||||
releaseFlash();
|
||||
return;
|
||||
}
|
||||
if (flushData()) flushMeta();
|
||||
releaseFlash();
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::reopen() {
|
||||
resetSession();
|
||||
if (!ensureFlash()) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
uint8_t header[8];
|
||||
if (!rawRead(0, header, sizeof(header)) || memcmp(header, MOTA_MAGIC, 4) != 0) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
uint32_t total = rd_u32le(header + 4);
|
||||
if (total < 13 || total > _flash_size) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
uint8_t trailer[5];
|
||||
if (!rawRead(total - sizeof(trailer), trailer, sizeof(trailer)) ||
|
||||
memcmp(trailer, MOTA_TRAILER, sizeof(trailer)) != 0) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
if (!rawRead(0, _meta_page, PAGE)) {
|
||||
fail("QSPI metadata load failed");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
_total = total;
|
||||
uint32_t pages = (total + PAGE - 1) / PAGE;
|
||||
for (uint32_t page = 0; page < pages; page++)
|
||||
setPageKnown(page);
|
||||
releaseFlash();
|
||||
return true;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::clear() {
|
||||
bool ok = ensureFlash();
|
||||
uint8_t zero[4] = { 0, 0, 0, 0 };
|
||||
uint8_t check[sizeof(zero)];
|
||||
if (ok) {
|
||||
ok = rawWrite(0, zero, sizeof(zero)) && rawRead(0, check, sizeof(check)) &&
|
||||
memcmp(check, zero, sizeof(zero)) == 0;
|
||||
}
|
||||
if (ok && _total >= 8 + MOTA_OFF_APPROVAL + sizeof(zero)) {
|
||||
const uint32_t approval = 8 + MOTA_OFF_APPROVAL;
|
||||
ok = rawWrite(approval, zero, sizeof(zero)) && rawRead(approval, check, sizeof(check)) &&
|
||||
memcmp(check, zero, sizeof(zero)) == 0;
|
||||
}
|
||||
char saved_error[sizeof(_error)];
|
||||
strncpy(saved_error, _error, sizeof(saved_error));
|
||||
saved_error[sizeof(saved_error) - 1] = 0;
|
||||
resetSession();
|
||||
if (!ok) fail(saved_error[0] ? saved_error : "QSPI container invalidation failed");
|
||||
releaseFlash();
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::approve_for_bootloader() {
|
||||
if (!finalize()) return false;
|
||||
const uint32_t approval = 8 + MOTA_OFF_APPROVAL;
|
||||
if (approval + sizeof(APPROVAL_YES) > _total || !rawWrite(approval, APPROVAL_YES, sizeof(APPROVAL_YES))) {
|
||||
fail("QSPI approval write failed");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
uint8_t check[sizeof(APPROVAL_YES)];
|
||||
if (!rawRead(approval, check, sizeof(check)) || memcmp(check, APPROVAL_YES, sizeof(check)) != 0) {
|
||||
fail("QSPI approval verify failed");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
memcpy(_meta_page + approval, APPROVAL_YES, sizeof(APPROVAL_YES));
|
||||
releaseFlash();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(OTA_QSPI_STORE) && defined(QSPIFLASH)
|
||||
#error "OTA_QSPI_STORE raw staging cannot share a QSPI chip with QSPIFLASH"
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(ETHERNET_ENABLED)
|
||||
#error "WisBlock SPI OTA staging cannot share the bus/chip-select with Ethernet"
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(OTA_SD_STORE)
|
||||
#error "WisBlock SPI OTA staging cannot share the bus/chip-select with SD staging"
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(RAK_3401)
|
||||
#error "RAK3401's RAK13302 radio already owns the WisBlock SPI bus/chip-select"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
|
||||
#include "OtaStore.h"
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// Persistent raw-QSPI staging for nRF52840 repeater builds. The complete .mota
|
||||
// lives at external-flash offset zero, leaving the whole internal application
|
||||
// region available to the bootloader as an in-place workspace. This store must
|
||||
// never be enabled on a build that mounts the same chip as a filesystem.
|
||||
class OtaStoreQspiNrf52 : public OtaStore {
|
||||
static const uint32_t PAGE = 4096;
|
||||
static const uint32_t PROGRAM = 256;
|
||||
static const uint32_t MAX_FLASH = 16UL * 1024 * 1024;
|
||||
static const uint32_t MAX_PAGES = MAX_FLASH / PAGE;
|
||||
static const uint32_t INVALID_PAGE = 0xFFFFFFFFUL;
|
||||
|
||||
uint32_t _total = 0;
|
||||
uint32_t _flash_size = 0;
|
||||
bool _qspi_active = false;
|
||||
bool _qspi_awake = false;
|
||||
bool _qspi_ready = false;
|
||||
bool _io_ok = true;
|
||||
bool _meta_dirty = false;
|
||||
bool _data_dirty = false;
|
||||
uint32_t _data_page_index = INVALID_PAGE;
|
||||
char _error[80] = { 0 };
|
||||
|
||||
alignas(4) uint8_t _meta_page[PAGE];
|
||||
alignas(4) uint8_t _data_page[PAGE];
|
||||
alignas(4) uint8_t _bounce[PROGRAM];
|
||||
uint8_t _known_pages[MAX_PAGES / 8];
|
||||
|
||||
void fail(const char *message);
|
||||
bool ensureFlash();
|
||||
void releaseFlash();
|
||||
bool waitReady(uint32_t timeout_ms);
|
||||
bool customInstruction(uint8_t opcode, uint8_t length, uint8_t *rx = nullptr);
|
||||
bool dmaReadAligned(uint32_t address, uint32_t length);
|
||||
bool dmaWriteAligned(uint32_t address, uint32_t length);
|
||||
bool rawRead(uint32_t address, void *data, uint32_t length);
|
||||
bool rawWrite(uint32_t address, const void *data, uint32_t length);
|
||||
bool rawErasePage(uint32_t address);
|
||||
bool flushPage(uint32_t page, const uint8_t *data);
|
||||
bool flushMeta();
|
||||
bool flushData();
|
||||
bool useDataPage(uint32_t page);
|
||||
void resetSession();
|
||||
bool pageKnown(uint32_t page) const;
|
||||
void setPageKnown(uint32_t page);
|
||||
|
||||
public:
|
||||
OtaStoreQspiNrf52();
|
||||
~OtaStoreQspiNrf52() override;
|
||||
|
||||
bool begin(uint32_t total_size) override;
|
||||
bool write(uint32_t offset, const uint8_t *data, uint32_t len) override;
|
||||
bool read(uint32_t offset, uint8_t *buf, uint32_t len) const override;
|
||||
uint32_t capacity() const override;
|
||||
uint32_t staged_size() const override { return _total; }
|
||||
void clear() override;
|
||||
bool set_meta_size(uint32_t meta_bytes) override;
|
||||
bool finalize() override;
|
||||
void checkpoint() override;
|
||||
bool reopen() override;
|
||||
bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off, uint32_t payload_size) override;
|
||||
|
||||
// Sets APRV only after all app-side verification gates have passed.
|
||||
bool approve_for_bootloader();
|
||||
const char *last_error() const { return _error; }
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
#endif
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand.
|
||||
// 552 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)
|
||||
// 566 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env 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;
|
||||
@@ -153,6 +153,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x21519537, "Heltec_v3_sensor" },
|
||||
{ 0x382bb181, "Heltec_v3_terminal_chat" },
|
||||
{ 0x5a2980e6, "heltec_v4_3_companion_radio_ble_femoff" },
|
||||
{ 0x90581c83, "heltec_v4_3_companion_radio_ble_ps_femoff" },
|
||||
{ 0x356df17c, "heltec_v4_3_companion_radio_usb_femoff" },
|
||||
{ 0x99a084fc, "heltec_v4_3_companion_radio_wifi_femoff" },
|
||||
{ 0x64c9dfc9, "heltec_v4_3_companion_radio_wifi_mqtt_femoff" },
|
||||
@@ -160,16 +161,23 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x2c13b8e9, "heltec_v4_3_tft_companion_radio_ble_femoff" },
|
||||
{ 0xb016979a, "heltec_v4_3_tft_companion_radio_usb_femoff" },
|
||||
{ 0xb49b6b55, "heltec_v4_3_tft_companion_radio_wifi_femoff" },
|
||||
{ 0x01d8f124, "heltec_v4_companion_radio_ble" },
|
||||
{ 0x86fb85f0, "heltec_v4_companion_radio_ble_femon" },
|
||||
{ 0xd33b0986, "heltec_v4_companion_radio_ble_ps_femon" },
|
||||
{ 0x1a4d0096, "heltec_v4_companion_radio_usb" },
|
||||
{ 0xa8f5940f, "heltec_v4_companion_radio_usb_femon" },
|
||||
{ 0x1cf5e14c, "heltec_v4_companion_radio_usb_ps_femoff" },
|
||||
{ 0x2707d3d1, "heltec_v4_companion_radio_usb_ps_femon" },
|
||||
{ 0x86ac3b1b, "heltec_v4_companion_radio_wifi_femon" },
|
||||
{ 0x7af848da, "heltec_v4_companion_radio_wifi_mqtt_femon" },
|
||||
{ 0xd522a14f, "heltec_v4_expansionkit_repeater" },
|
||||
{ 0x76657d76, "heltec_v4_expansionkit_repeater_observer_mqtt" },
|
||||
{ 0x21ee03bd, "heltec_v4_expansionkit_room_server_observer_mqtt" },
|
||||
{ 0x5977d941, "heltec_v4_expansionkit_tft_companion_radio_ble_femon" },
|
||||
{ 0xe2a1907b, "heltec_v4_expansionkit_tft_companion_radio_ble_ps" },
|
||||
{ 0xf75feb3e, "heltec_v4_kiss_modem" },
|
||||
{ 0xbc9cdc70, "heltec_v4_r8_companion_radio_ble" },
|
||||
{ 0x19e31fc6, "heltec_v4_r8_companion_radio_ble_ps" },
|
||||
{ 0x36c78d86, "heltec_v4_r8_companion_radio_usb" },
|
||||
{ 0xdf722326, "heltec_v4_r8_companion_radio_wifi" },
|
||||
{ 0xfa901c87, "heltec_v4_r8_kiss_modem" },
|
||||
@@ -411,6 +419,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x76f3c984, "RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors" },
|
||||
{ 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" },
|
||||
{ 0xf1d3c5a8, "RAK_4631_terminal_chat" },
|
||||
{ 0xc6d55752, "RAK_WisMesh_Tag_companion_radio_ble" },
|
||||
{ 0x60683191, "RAK_WisMesh_Tag_companion_radio_usb" },
|
||||
@@ -442,7 +451,9 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x0ba4453d, "Station_G3_ESP32_kiss_modem" },
|
||||
{ 0xf8d1958f, "Station_G3_ESP32_logging_repeater" },
|
||||
{ 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" },
|
||||
{ 0x4dbf035e, "t1000e_companion_radio_ble" },
|
||||
{ 0xff4102d6, "t1000e_companion_radio_usb" },
|
||||
{ 0x2f9a9185, "t1000e_kiss_modem" },
|
||||
@@ -517,7 +528,9 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0xa5f4a562, "ThinkNode_M7_companion_radio_wifi" },
|
||||
{ 0xef83222b, "ThinkNode_M7_kiss_modem" },
|
||||
{ 0xc36ec867, "ThinkNode_M7_repeater" },
|
||||
{ 0xb932238e, "ThinkNode_M7_repeater_observer_mqtt" },
|
||||
{ 0x92f5bbea, "ThinkNode_M7_room_server" },
|
||||
{ 0x26d91ab2, "ThinkNode_M7_room_server_observer_mqtt" },
|
||||
{ 0x708ac4dc, "ThinkNode_M9_companion_radio_ble_" },
|
||||
{ 0xdc51bf13, "ThinkNode_M9_companion_radio_usb_" },
|
||||
{ 0xc27f8304, "ThinkNode_M9_companion_radio_wifi_" },
|
||||
@@ -530,6 +543,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x475a2a6a, "WioTrackerL1_companion_radio_ble" },
|
||||
{ 0xf4175d2c, "WioTrackerL1_companion_radio_usb" },
|
||||
{ 0x3d831006, "WioTrackerL1_kiss_modem" },
|
||||
{ 0x556eb1bc, "WioTrackerL1_repeater" },
|
||||
{ 0xb67370b3, "WioTrackerL1_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x0c689b90, "WioTrackerL1_room_server" },
|
||||
{ 0x91eac424, "WioTrackerL1Eink_companion_radio_ble" },
|
||||
|
||||
@@ -36,6 +36,30 @@ TEST(OtaFlashPlan, SelectsCeilingFromLinkedLayoutAndStorage) {
|
||||
EXPECT_EQ(mota_nrf52_stage_ceiling_for_layout(0xE0000u, false), LEGACY);
|
||||
}
|
||||
|
||||
TEST(OtaFlashPlan, ValidatesExternalInplacePatchGeometryBeforeHandoff) {
|
||||
const uint32_t workspace = EXPANDED - APP_V7;
|
||||
const uint32_t running = 620000u;
|
||||
const uint32_t target = 633984u;
|
||||
ASSERT_TRUE(mota_nrf52_external_patch_geometry_valid(
|
||||
workspace, MOTA_NRF52_FLASH_PAGE, 0, running, target,
|
||||
workspace, running, target));
|
||||
|
||||
EXPECT_FALSE(mota_nrf52_external_patch_geometry_valid(
|
||||
workspace + 1, MOTA_NRF52_FLASH_PAGE, 0, running, target,
|
||||
workspace, running, target));
|
||||
EXPECT_FALSE(mota_nrf52_external_patch_geometry_valid(
|
||||
workspace, 2048, 0, running, target, workspace, running, target));
|
||||
EXPECT_FALSE(mota_nrf52_external_patch_geometry_valid(
|
||||
workspace, MOTA_NRF52_FLASH_PAGE, 1, running, target,
|
||||
workspace, running, target));
|
||||
EXPECT_FALSE(mota_nrf52_external_patch_geometry_valid(
|
||||
workspace, MOTA_NRF52_FLASH_PAGE, 0, running - 1, target,
|
||||
workspace, running, target));
|
||||
EXPECT_FALSE(mota_nrf52_external_patch_geometry_valid(
|
||||
workspace, MOTA_NRF52_FLASH_PAGE, 0, running, target + 1,
|
||||
workspace, running, target));
|
||||
}
|
||||
|
||||
// A typical running image (~520 KB) leaves room; the container lands strictly above it and below ExtraFS.
|
||||
TEST(OtaFlashPlan, StagesBelowFilesystemAndAboveApp) {
|
||||
uint32_t start = 0xDEADBEEF;
|
||||
|
||||
+48
-10
@@ -151,6 +151,12 @@ class TargetInfo:
|
||||
self_status: str
|
||||
current_version: str | None = None
|
||||
current_version_source: str | None = None
|
||||
nrf_qspi: bool = False
|
||||
|
||||
@property
|
||||
def nrf_external(self) -> bool:
|
||||
"""Whether nRF52 OTA staging is outside internal application flash."""
|
||||
return self.nrf_sd or self.nrf_qspi
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -635,7 +641,7 @@ def compatible_mota(info: MotaInfo, target: TargetInfo) -> tuple[bool, str]:
|
||||
if info.hw_id and target.hw_id and info.hw_id != target.hw_id:
|
||||
return False, f"hardware {info.hw_id!r}, destination is {target.hw_id!r}"
|
||||
if info.is_full:
|
||||
if target.platform == "nrf52" and not target.nrf_sd:
|
||||
if target.platform == "nrf52" and not target.nrf_external:
|
||||
return False, "internal-flash nRF52 accepts only an in-place delta"
|
||||
else:
|
||||
if info.base_hash != target.base_hash:
|
||||
@@ -943,7 +949,9 @@ def prepare_package(
|
||||
"--out",
|
||||
str(output),
|
||||
]
|
||||
if target.platform == "nrf52" and (not target.nrf_sd or args.base is not None):
|
||||
if target.platform == "nrf52" and (
|
||||
not target.nrf_external or args.base is not None
|
||||
):
|
||||
if args.base is None:
|
||||
raise OtaError(
|
||||
"this nRF52 ZIP contains raw firmware, not a ready delta mOTA; "
|
||||
@@ -958,8 +966,12 @@ def prepare_package(
|
||||
"--patch-type",
|
||||
"in-place",
|
||||
])
|
||||
# External staging leaves the application region available as the
|
||||
# detools workspace. 0xC6000 is safe for both S140 v7 (app starts
|
||||
# at 0x27000) and older v6 layouts (0x26000). Internal staging
|
||||
# retains its deliberately smaller legacy workspace.
|
||||
inplace_memory = args.inplace_memory or (
|
||||
"0xC7000" if target.nrf_sd else "0x98000"
|
||||
"0xC6000" if target.nrf_external else "0x98000"
|
||||
)
|
||||
command.extend(["--inplace-memory", inplace_memory])
|
||||
if args.sign_key:
|
||||
@@ -1597,9 +1609,22 @@ def query_target(
|
||||
nrf_sd = "SD apply OK" in combined or bool(
|
||||
re.search(r"\bbl:SD\b", combined)
|
||||
)
|
||||
nrf_qspi = "QSPI apply OK" in combined or bool(
|
||||
re.search(r"\bbl:QSPI\b", combined)
|
||||
)
|
||||
qspi_store = re.search(r"\bQSPI store:(ERR\s+)?(\d+)K\b", combined)
|
||||
if nrf_qspi and qspi_store and (
|
||||
qspi_store.group(1) is not None or int(qspi_store.group(2)) == 0
|
||||
):
|
||||
raise OtaError(
|
||||
"destination bootloader supports QSPI apply, but the application "
|
||||
"reports `QSPI store:ERR 0K`; check the exact-board firmware, "
|
||||
"flash wiring, and flash power before downloading"
|
||||
)
|
||||
if platform == "nrf52" and (
|
||||
"NO mota-apply" in combined
|
||||
or "NO SD mota-apply" in combined
|
||||
or "NO QSPI mota-apply" in combined
|
||||
or bool(re.search(r"\bbl:NONE\b", combined))
|
||||
):
|
||||
version = bootloader_version or "unknown version"
|
||||
@@ -1659,6 +1684,7 @@ def query_target(
|
||||
self_status=self_status,
|
||||
current_version=current_version,
|
||||
current_version_source=current_version_source,
|
||||
nrf_qspi=nrf_qspi,
|
||||
)
|
||||
|
||||
|
||||
@@ -2897,7 +2923,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
"--base", type=Path,
|
||||
help=(
|
||||
"exact running .bin/.hex/.zip/full.mota (required to build an "
|
||||
"internal-flash nRF52 delta; optional for SD-backed nRF52)"
|
||||
"internal-flash nRF52 delta; optional for external SD/QSPI nRF52)"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--zip-member", help="select one exact path inside PACKAGE ZIP")
|
||||
@@ -2905,7 +2931,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("--public-key", type=Path, help="require this signer when verifying")
|
||||
parser.add_argument(
|
||||
"--inplace-memory",
|
||||
help="nRF52 OTAFIX workspace (auto: 0x98000 internal, 0xC7000 SD)",
|
||||
help="nRF52 OTAFIX workspace (auto: 0x98000 internal, 0xC6000 external SD/QSPI)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--platform", choices=("esp32", "nrf52"),
|
||||
@@ -2987,6 +3013,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("--target-id", help="8-hex target ID for --prepare-only")
|
||||
parser.add_argument("--target-base-hash", help="16-hex EndF body hash for --prepare-only")
|
||||
parser.add_argument("--nrf-sd", action="store_true", help="offline target uses nRF52 SD staging")
|
||||
parser.add_argument(
|
||||
"--nrf-qspi", action="store_true",
|
||||
help="offline target uses nRF52 external QSPI staging",
|
||||
)
|
||||
parser.add_argument("--target-hw", help="hardware identity for --prepare-only")
|
||||
parser.add_argument(
|
||||
"--allow-non-upgrade", action="store_true",
|
||||
@@ -3038,15 +3068,22 @@ def validate_args(args: argparse.Namespace, parser: argparse.ArgumentParser) ->
|
||||
if args.prepare_only:
|
||||
if not args.platform or not args.target_id:
|
||||
parser.error("--prepare-only requires --platform and --target-id")
|
||||
if args.platform == "nrf52" and not args.nrf_sd and not args.target_base_hash:
|
||||
if (
|
||||
args.platform == "nrf52"
|
||||
and not (args.nrf_sd or args.nrf_qspi)
|
||||
and not args.target_base_hash
|
||||
):
|
||||
parser.error("offline internal-flash nRF52 preparation requires --target-base-hash")
|
||||
if args.nrf_sd and args.platform != "nrf52":
|
||||
parser.error("--nrf-sd requires --platform nrf52")
|
||||
if args.nrf_sd and args.nrf_qspi:
|
||||
parser.error("--nrf-sd and --nrf-qspi are mutually exclusive")
|
||||
if (args.nrf_sd or args.nrf_qspi) and args.platform != "nrf52":
|
||||
parser.error("--nrf-sd/--nrf-qspi require --platform nrf52")
|
||||
else:
|
||||
if any((args.platform, args.target_id, args.target_base_hash, args.target_hw, args.nrf_sd)):
|
||||
if any((args.platform, args.target_id, args.target_base_hash,
|
||||
args.target_hw, args.nrf_sd, args.nrf_qspi)):
|
||||
parser.error(
|
||||
"--platform, --target-id, --target-base-hash, --target-hw, and "
|
||||
"--nrf-sd are only valid with --prepare-only"
|
||||
"--nrf-sd/--nrf-qspi are only valid with --prepare-only"
|
||||
)
|
||||
if not any((args.controller_serial, args.controller_tcp, args.controller_ble)):
|
||||
parser.error("a controller connection is required")
|
||||
@@ -3177,6 +3214,7 @@ def offline_target(args: argparse.Namespace) -> TargetInfo:
|
||||
bootloader_codecs=None,
|
||||
status="offline",
|
||||
self_status="offline",
|
||||
nrf_qspi=args.nrf_qspi,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -837,7 +837,7 @@ def query_live_target(
|
||||
raise ota.OtaError(
|
||||
f"live hardware is {target.hw_id!r}, expected {EXPECTED_HARDWARE!r}"
|
||||
)
|
||||
if target.platform != "nrf52" or target.nrf_sd:
|
||||
if target.platform != "nrf52" or target.nrf_external:
|
||||
raise ota.OtaError("live target is not the expected internal-flash nRF52 node")
|
||||
if target.bootloader_abi is None or target.bootloader_abi < 2:
|
||||
raise ota.OtaError("live target does not report OTAFIX mOTA ABI 2")
|
||||
|
||||
@@ -85,6 +85,7 @@ def target(
|
||||
platform: str = "esp32",
|
||||
base_hash: bytes = b"\0" * 8,
|
||||
nrf_sd: bool = False,
|
||||
nrf_qspi: bool = False,
|
||||
boot_codecs: int | None = None,
|
||||
boot_version: str | None = None,
|
||||
current_version: str | None = None,
|
||||
@@ -102,6 +103,7 @@ def target(
|
||||
status="status",
|
||||
self_status="self",
|
||||
current_version=current_version,
|
||||
nrf_qspi=nrf_qspi,
|
||||
)
|
||||
|
||||
|
||||
@@ -177,6 +179,23 @@ class FormatTests(unittest.TestCase):
|
||||
])
|
||||
ota.validate_args(args, parser)
|
||||
|
||||
def test_offline_qspi_nrf52_does_not_require_a_base_hash(self) -> None:
|
||||
parser = ota.build_parser()
|
||||
args = parser.parse_args([
|
||||
"release.zip", "offline", "--prepare-only", "--platform", "nrf52",
|
||||
"--target-id", f"{TARGET:08X}", "--nrf-qspi",
|
||||
])
|
||||
ota.validate_args(args, parser)
|
||||
|
||||
def test_nrf_external_stores_are_mutually_exclusive(self) -> None:
|
||||
parser = ota.build_parser()
|
||||
args = parser.parse_args([
|
||||
"release.zip", "offline", "--prepare-only", "--platform", "nrf52",
|
||||
"--target-id", f"{TARGET:08X}", "--nrf-sd", "--nrf-qspi",
|
||||
])
|
||||
with self.assertRaises(SystemExit), contextlib.redirect_stderr(io.StringIO()):
|
||||
ota.validate_args(args, parser)
|
||||
|
||||
def test_nrf_sd_is_rejected_for_esp32(self) -> None:
|
||||
parser = ota.build_parser()
|
||||
args = parser.parse_args([
|
||||
@@ -401,6 +420,11 @@ class CompatibilityTests(unittest.TestCase):
|
||||
nrf = target(platform="nrf52", nrf_sd=True, boot_codecs=1)
|
||||
self.assertTrue(ota.compatible_mota(full, nrf)[0])
|
||||
|
||||
def test_qspi_nrf52_accepts_full_when_bootloader_does(self) -> None:
|
||||
full = ota.parse_mota(mota_blob(self.new_image))
|
||||
nrf = target(platform="nrf52", nrf_qspi=True, boot_codecs=1)
|
||||
self.assertTrue(ota.compatible_mota(full, nrf)[0])
|
||||
|
||||
def test_zip_prefers_equal_version_delta(self) -> None:
|
||||
full = mota_blob(self.new_image)
|
||||
delta = mota_blob(
|
||||
@@ -697,6 +721,48 @@ class ReliabilityTests(unittest.TestCase):
|
||||
["ota status", "get bootloader.ver", "ota self", "ota stats"],
|
||||
)
|
||||
|
||||
def test_target_detects_qspi_staging(self) -> None:
|
||||
class Controller:
|
||||
def __init__(self) -> None:
|
||||
self.replies = iter([
|
||||
"OTA | no download | target:1234ABCD hw=Xiao_nrf52 | bl:QSPI blrc:B0",
|
||||
"> 0.9.2-OTAFIX2.4",
|
||||
"self body=1 image=2 base_hash=0011223344556677 | "
|
||||
"bootloader: QSPI apply OK (abi=2 codecs=0x5)",
|
||||
"OTA | fw v1.17.0 id=00112233",
|
||||
])
|
||||
|
||||
def remote_command(self, *_args: object, **_kwargs: object) -> str:
|
||||
return next(self.replies)
|
||||
|
||||
result = ota.query_target(
|
||||
Controller(), argparse.Namespace(target="remote")
|
||||
)
|
||||
self.assertTrue(result.nrf_qspi)
|
||||
self.assertTrue(result.nrf_external)
|
||||
self.assertFalse(result.nrf_sd)
|
||||
|
||||
def test_target_rejects_unavailable_qspi_store(self) -> None:
|
||||
class Controller:
|
||||
def __init__(self) -> None:
|
||||
self.replies = iter([
|
||||
"OTA | no download | target:1234ABCD hw=Xiao_nrf52 | bl:QSPI blrc:B0",
|
||||
"> 0.9.2-OTAFIX2.4",
|
||||
"self body=1 image=2 base_hash=0011223344556677 | "
|
||||
"QSPI store:ERR 0K | bootloader: QSPI apply OK "
|
||||
"(abi=2 codecs=0x5)",
|
||||
])
|
||||
|
||||
def remote_command(self, *_args: object, **_kwargs: object) -> str:
|
||||
return next(self.replies)
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
ota.OtaError, "bootloader supports QSPI apply.*QSPI store:ERR 0K"
|
||||
):
|
||||
ota.query_target(
|
||||
Controller(), argparse.Namespace(target="remote")
|
||||
)
|
||||
|
||||
def test_stock_nrf52_bootloader_reports_required_action(self) -> None:
|
||||
class Controller:
|
||||
def __init__(self) -> None:
|
||||
@@ -2030,6 +2096,22 @@ class MotatoolIntegrationTests(unittest.TestCase):
|
||||
)
|
||||
self.assertTrue(package.is_full)
|
||||
|
||||
def test_raw_qspi_nrf52_zip_becomes_full_without_base(self) -> None:
|
||||
image = firmware(b"nrf-qspi-new" * 800, VERSION_NEW)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
archive_path = root / "release.zip"
|
||||
with zipfile.ZipFile(archive_path, "w") as archive:
|
||||
archive.writestr("firmware.bin", image)
|
||||
work = root / "work"
|
||||
work.mkdir()
|
||||
_path, package, _expected = ota.prepare_package(
|
||||
prepare_args(archive_path, self.motatool),
|
||||
target(platform="nrf52", nrf_qspi=True, boot_codecs=1),
|
||||
work,
|
||||
)
|
||||
self.assertTrue(package.is_full)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
+26
-2
@@ -72,6 +72,7 @@ NRF52_LAYOUT_VERSION = 1
|
||||
NRF52_LAYOUT_LEN = 24
|
||||
NRF52_LAYOUT_FLAG_SD = 0x01
|
||||
NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS = 0x02
|
||||
NRF52_LAYOUT_FLAG_QSPI = 0x04
|
||||
|
||||
# MeshTower V2's SD-backed OTA target keeps the staged .mota off-chip, so the application may use the
|
||||
# complete S140 v6 application region up to InternalFS instead of leaving room for internal staging.
|
||||
@@ -140,6 +141,15 @@ def hardware_id_for_env(env_name: str) -> str:
|
||||
possible, while a cross-board install is rejected. Long family names retain a short hash suffix to
|
||||
avoid collisions inside EndF's fixed 32-byte field.
|
||||
"""
|
||||
# Optional storage that requires a different bootloader/application pair is
|
||||
# a distinct hardware class even though it uses the same WisBlock Core.
|
||||
if re.fullmatch(
|
||||
r"RAK_4631_repeater_rak15001_slot_c_lora_ota",
|
||||
env_name,
|
||||
re.IGNORECASE,
|
||||
):
|
||||
return "RAK4631_RAK15001_C"
|
||||
|
||||
role = re.search(
|
||||
r"[_-](?:repeater|repeatr|room_server|room_svr|sensor|terminal_chat|kiss_modem|"
|
||||
r"companion_radio|companion|comp_radio)(?=[_-]|$)", env_name, re.IGNORECASE)
|
||||
@@ -181,6 +191,14 @@ class Nrf52Layout:
|
||||
def sd_backed(self) -> bool:
|
||||
return bool(self.flags & NRF52_LAYOUT_FLAG_SD)
|
||||
|
||||
@property
|
||||
def qspi_backed(self) -> bool:
|
||||
return bool(self.flags & NRF52_LAYOUT_FLAG_QSPI)
|
||||
|
||||
@property
|
||||
def external_backed(self) -> bool:
|
||||
return self.sd_backed or self.qspi_backed
|
||||
|
||||
|
||||
def nrf52_stage_ceiling_for_layout(linked_app_end: int, uses_internal_extrafs: bool) -> int:
|
||||
"""Select a safe staging ceiling from linker geometry and actual secondary-storage type."""
|
||||
@@ -200,9 +218,15 @@ def build_nrf52_layout(layout: Nrf52Layout) -> bytes:
|
||||
raise ValueError(f"unsupported nRF52 staging ceiling 0x{layout.stage_ceiling:X}")
|
||||
if not (layout.app_base < layout.linked_app_end <= NRF52_APP_END):
|
||||
raise ValueError("invalid nRF52 app region")
|
||||
if layout.flags & ~(NRF52_LAYOUT_FLAG_SD | NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS):
|
||||
known_flags = (NRF52_LAYOUT_FLAG_SD | NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS |
|
||||
NRF52_LAYOUT_FLAG_QSPI)
|
||||
if layout.flags & ~known_flags:
|
||||
raise ValueError(f"unsupported nRF52 layout flags 0x{layout.flags:X}")
|
||||
expected_ceiling = (NRF52_APP_END if layout.sd_backed else
|
||||
if layout.sd_backed and layout.qspi_backed:
|
||||
raise ValueError("nRF52 layout cannot use both SD and QSPI staging")
|
||||
if layout.external_backed and layout.flags & NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS:
|
||||
raise ValueError("nRF52 external staging cannot also reserve internal ExtraFS")
|
||||
expected_ceiling = (NRF52_APP_END if layout.external_backed else
|
||||
nrf52_stage_ceiling_for_layout(
|
||||
layout.linked_app_end,
|
||||
bool(layout.flags & NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS)))
|
||||
|
||||
@@ -178,9 +178,15 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f
|
||||
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
|
||||
stage_ceiling = (ml.NRF52_APP_END if sd_backed else
|
||||
qspi_backed = _cppdef("OTA_QSPI_STORE") is not None
|
||||
if sd_backed and qspi_backed:
|
||||
raise RuntimeError("nRF52 build cannot enable both SD and QSPI OTA stores")
|
||||
if qspi_backed and _cppdef("QSPIFLASH") is not None:
|
||||
raise RuntimeError("raw QSPI OTA staging cannot share a chip with QSPIFLASH")
|
||||
stage_ceiling = (ml.NRF52_APP_END if (sd_backed or qspi_backed) else
|
||||
ml.nrf52_stage_ceiling_for_layout(linked_app_end, internal_extrafs))
|
||||
layout_flags = ((ml.NRF52_LAYOUT_FLAG_SD if sd_backed else 0) |
|
||||
(ml.NRF52_LAYOUT_FLAG_QSPI if qspi_backed else 0) |
|
||||
(ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS if internal_extrafs else 0))
|
||||
layout = ml.Nrf52Layout(app_start, linked_app_end, stage_ceiling, layout_flags)
|
||||
body = ml.ensure_nrf52_layout(raw_body, layout)
|
||||
|
||||
@@ -54,6 +54,14 @@ def test_ota_target_generation_honors_explicit_disable():
|
||||
def test_hardware_id_for_env():
|
||||
assert ml.hardware_id_for_env("RAK_4631_repeater") == "RAK4631"
|
||||
assert ml.hardware_id_for_env("RAK_4631_companion_radio_usb") == "RAK4631"
|
||||
assert (
|
||||
ml.hardware_id_for_env("RAK_4631_repeater_rak15001_slot_c_lora_ota")
|
||||
== "RAK4631_RAK15001_C"
|
||||
)
|
||||
assert (
|
||||
ml.hardware_id_for_env("RAK_3401_repeater_rak15001_slot_c_lora_ota")
|
||||
== "RAK_3401"
|
||||
)
|
||||
assert (
|
||||
ml.hardware_id_for_env("Heltec_t114_without_display_repeater")
|
||||
== "Heltec_t114"
|
||||
@@ -128,6 +136,12 @@ def test_nrf52_layout_record_roundtrip_and_policy():
|
||||
ml.NRF52_EXTRAFS_START, ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS)
|
||||
internal_image, _ = ml.ensure_endf(ml.ensure_nrf52_layout(_fw(6, 2048), internal))
|
||||
assert ml.parse_nrf52_layout(internal_image) == internal
|
||||
qspi = ml.Nrf52Layout(
|
||||
ml.NRF52_APP_BASE_S140_V7, ml.NRF52_APP_END,
|
||||
ml.NRF52_APP_END, ml.NRF52_LAYOUT_FLAG_QSPI)
|
||||
qspi_image, _ = ml.ensure_endf(ml.ensure_nrf52_layout(_fw(7, 2048), qspi))
|
||||
assert ml.parse_nrf52_layout(qspi_image) == qspi
|
||||
assert qspi.qspi_backed and qspi.external_backed and not qspi.sd_backed
|
||||
try:
|
||||
ml.build_nrf52_layout(ml.Nrf52Layout(
|
||||
ml.NRF52_APP_BASE_S140_V7, ml.NRF52_EXTRAFS_START,
|
||||
@@ -135,6 +149,17 @@ def test_nrf52_layout_record_roundtrip_and_policy():
|
||||
assert False, "inconsistent layout record accepted"
|
||||
except ValueError:
|
||||
pass
|
||||
for flags in (
|
||||
ml.NRF52_LAYOUT_FLAG_SD | ml.NRF52_LAYOUT_FLAG_QSPI,
|
||||
ml.NRF52_LAYOUT_FLAG_QSPI | ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS,
|
||||
):
|
||||
try:
|
||||
ml.build_nrf52_layout(ml.Nrf52Layout(
|
||||
ml.NRF52_APP_BASE_S140_V7, ml.NRF52_APP_END,
|
||||
ml.NRF52_APP_END, flags))
|
||||
assert False, "conflicting nRF52 layout flags accepted"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
# --- merkle ----------------------------------------------------------------
|
||||
|
||||
@@ -89,6 +89,7 @@ build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter}
|
||||
[env:ikoka_handheld_nrf_e22_30dbm_repeater]
|
||||
extends = ikoka_handheld_nrf
|
||||
build_flags = ${ikoka_handheld_nrf.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ADVERT_NAME='"ikoka_handheld Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
|
||||
@@ -120,6 +120,7 @@ lib_deps =
|
||||
extends = ikoka_nano_nrf
|
||||
build_flags =
|
||||
${ikoka_nano_nrf.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ADVERT_NAME='"Ikoka Nano Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
|
||||
@@ -126,6 +126,7 @@ lib_deps =
|
||||
extends = ikoka_stick_nrf
|
||||
build_flags =
|
||||
${ikoka_stick_nrf.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ADVERT_NAME='"Ikoka Stick Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
|
||||
@@ -56,6 +56,7 @@ build_src_filter = ${LilyGo_T-Echo.build_src_filter}
|
||||
+<../examples/simple_repeater>
|
||||
build_flags =
|
||||
${LilyGo_T-Echo.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ADVERT_NAME='"T-Echo Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
|
||||
@@ -48,7 +48,9 @@ build_src_filter = ${rak3401.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
; Explicit lean alternative to the full-sensor, non-OTA repeater above.
|
||||
; Explicit lean alternative to the full-sensor, non-OTA repeater above. This
|
||||
; profile also removes ENV_INCLUDE_GPS: use RAK_3401_repeater with a RAK12501
|
||||
; in sensor slot A when GPS is required (slot D conflicts with radio BUSY/DIO1).
|
||||
[env:RAK_3401_repeater_lora_ota_no_external_sensors]
|
||||
extends = rak3401
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
|
||||
@@ -64,6 +64,29 @@ build_src_filter = ${rak4631.build_src_filter}
|
||||
; Shared sources remain inherited, but ENABLE_OTA is explicitly undefined for this oversized role.
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
; Full-sensor/GPS repeater for a RAK4631 fitted with a 2 MiB RAK15001 in sensor
|
||||
; slot C. Its WP#/HOLD# inputs have onboard pull-ups, so firmware does not drive
|
||||
; slot GPIO. Do not use this target with RAK13800 Ethernet, RAK15002 SD, or
|
||||
; another SPI module.
|
||||
[env:RAK_4631_repeater_rak15001_slot_c_lora_ota]
|
||||
extends = rak4631
|
||||
build_unflags =
|
||||
-D MOTA_HW_ID='"RAK4631"'
|
||||
-D OTA_FLASH_STORE=1
|
||||
build_flags =
|
||||
${rak4631.build_flags}
|
||||
${nrf52_rak15001_ota.build_flags}
|
||||
-D MOTA_HW_ID='"RAK4631_RAK15001_C"'
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"RAK4631 + RAK15001 OTA"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
build_src_filter = ${rak4631.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
|
||||
; Lean OTA target for space-constrained RAK4631 repeaters. This removes only optional external
|
||||
; environmental/GPS sensor packages. RAK4631Board::getBattMilliVolts(), repeater telemetry, and
|
||||
; battery.alert remain part of the board/repeater sources below.
|
||||
|
||||
@@ -41,6 +41,7 @@ extends = SenseCap_Solar
|
||||
extra_scripts = ${nrf52_lora_ota.extra_scripts}
|
||||
build_flags =
|
||||
${SenseCap_Solar.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ENV_INCLUDE_GPS=1 ; onboard GPS is available to repeater and LoRa-OTA builds
|
||||
-D ENV_SKIP_GPS_DETECT=1 ; onboard L76KB may not emit NMEA during the one-second boot probe
|
||||
-D ADVERT_NAME='"SenseCap_Solar Repeater"'
|
||||
|
||||
@@ -39,6 +39,7 @@ upload_protocol = nrfutil
|
||||
extends = ThinkNode_M1
|
||||
build_flags =
|
||||
${ThinkNode_M1.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ADVERT_NAME='"ThinkNode Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
|
||||
@@ -42,6 +42,7 @@ upload_protocol = nrfutil
|
||||
extends = ThinkNode_M6
|
||||
build_flags =
|
||||
${ThinkNode_M6.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ADVERT_NAME='"ThinkNode Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
|
||||
@@ -14,7 +14,13 @@ void initVariant() {
|
||||
digitalWrite(PIN_PWR_EN, HIGH);
|
||||
|
||||
pinMode(QSPI_FLASH_EN, OUTPUT);
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
// Raw OTA staging wakes and identifies the NOR only while an operation is
|
||||
// active. Start power-saving repeater builds with the flash rail off.
|
||||
digitalWrite(QSPI_FLASH_EN, LOW);
|
||||
#else
|
||||
digitalWrite(QSPI_FLASH_EN, HIGH);
|
||||
#endif
|
||||
|
||||
// For now stick adc_ctrl to fixed value
|
||||
pinMode(PIN_ADC_CTRL, OUTPUT);
|
||||
|
||||
@@ -32,8 +32,7 @@ build_src_filter = ${WioTrackerL1.build_src_filter}
|
||||
+<../examples/simple_repeater>
|
||||
build_flags =
|
||||
${WioTrackerL1.build_flags}
|
||||
-UENABLE_OTA
|
||||
-D DISABLE_LORA_OTA=1 ; Full external-sensor build exceeds the in-place OTA ceiling
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ADVERT_NAME='"WioTrackerL1 Repeater"'
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
@@ -52,6 +51,7 @@ build_src_filter = ${WioTrackerL1.build_src_filter}
|
||||
+<../examples/simple_repeater>
|
||||
build_flags =
|
||||
${WioTrackerL1.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
${nrf52_no_external_sensors_keep_gps.build_flags}
|
||||
-D ENV_INCLUDE_GPS=1 ; onboard GPS remains available in the lean LoRa-OTA build
|
||||
-D ADVERT_NAME='"WioTrackerL1 Repeater"'
|
||||
|
||||
@@ -92,6 +92,7 @@ lib_deps =
|
||||
extends = Xiao_nrf52
|
||||
build_flags =
|
||||
${Xiao_nrf52.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ADVERT_NAME='"Xiao_nrf52 Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -199,6 +200,7 @@ build_flags =
|
||||
extends = solarxiao
|
||||
build_flags =
|
||||
${solarxiao.build_flags}
|
||||
${nrf52_qspi_ota.build_flags}
|
||||
-D ADVERT_NAME='"SolarXiao 30S Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
|
||||
Reference in New Issue
Block a user