Add external QSPI staging for nRF52 OTA

This commit is contained in:
mikecarper
2026-08-20 17:51:52 -07:00
parent 56afcb8fd9
commit 509f831442
40 changed files with 1671 additions and 157 deletions
@@ -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
+41 -9
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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)
+18 -7
View File
@@ -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
View File
@@ -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
+190
View File
@@ -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
View File
@@ -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
View File
@@ -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.
---
+10
View File
@@ -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.
+26
View File
@@ -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
+64 -17
View File
@@ -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; }
+5
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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(); }
+15 -11
View File
@@ -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)
+15
View File
@@ -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 ||
+701
View File
@@ -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
+95
View File
@@ -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
+15 -1
View File
@@ -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" },
+24
View File
@@ -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
View File
@@ -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,
)
+1 -1
View File
@@ -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")
+82
View File
@@ -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
View File
@@ -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)))
+7 -1
View File
@@ -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)
+25
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+3 -1
View File
@@ -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}
+23
View File
@@ -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.
+1
View File
@@ -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"'
+1
View File
@@ -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
+1
View File
@@ -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
+6
View File
@@ -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);
+2 -2
View File
@@ -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"'
+2
View File
@@ -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