Add Wi-Fi OTA partition migration support
This commit is contained in:
1 parent
69681a2db6
commit
9844982b00
20 files changed
+1637
-62
No files matched your search
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default.csv",
|
||||
"memory_type": "qio_qspi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "qspi",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "heltec_v4"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "Heltec V4 Wi-Fi partition migrator (16 MB FLASH, 2 MB PSRAM)",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 2097152,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": true,
|
||||
"wait_for_upload_port": true,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://heltec.org/",
|
||||
"vendor": "heltec"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_XIAO_ESP32S3",
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=0",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"mcu": "esp32s3",
|
||||
"variant": "XIAO_ESP32S3"
|
||||
},
|
||||
"connectivity": ["bluetooth", "wifi"],
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "Seeed XIAO ESP32-S3 Wi-Fi partition migrator (8 MB FLASH)",
|
||||
"upload": {
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 8388608,
|
||||
"require_upload_port": true,
|
||||
"speed": 460800
|
||||
},
|
||||
"url": "https://www.seeedstudio.com/XIAO-ESP32S3-p-5627.html",
|
||||
"vendor": "Seeed Studio"
|
||||
}
|
||||
@@ -79,7 +79,7 @@ BUILD_BACKGROUND_HANDOFF_STATE=""
|
||||
BUILD_SCRIPT_LOCK_FD=""
|
||||
INTERACTIVE_BUILD_SELECTION=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_rak13302_w25q16_lora_ota|repeater_rak15001_slot_c_lora_ota|repeater_w25q16_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'
|
||||
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_rak13302_w25q16_lora_ota|repeater_rak15001_slot_c_lora_ota|repeater_w25q16_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_lora_ota_no_external_sensors|room_svr_lora_ota_no_external_sensors|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"
|
||||
@@ -119,7 +119,7 @@ Commands:
|
||||
list|-l: List firmwares available to build.
|
||||
build-firmware <target>: Build the firmware for the given build target.
|
||||
build-firmwares: Build canonical firmwares for all targets. Runtime-setting aliases and Terminal Chat targets replaced by Full Companion remain available as explicit builds.
|
||||
build-firmwares-logging-matrix: Build canonical standard artifacts with merged runtime USB logging plus unified FULL ESP32 USB+WiFi and FULL fallback profiles, logging each target under out/build-logs/ and continuing after failures. MQTT observers and ESP-NOW bridges always use FULL. KISS, BLE-only Companion, and constrained LoRa-OTA repeater contracts do not gain plaintext USB logging.
|
||||
build-firmwares-logging-matrix: Build canonical standard artifacts with merged runtime USB logging plus unified FULL ESP32 USB+WiFi and FULL fallback profiles, logging each target under out/build-logs/ and continuing after failures. MQTT observers and ESP-NOW bridges always use FULL. KISS, BLE-only Companion, and constrained LoRa-OTA repeater/room-server contracts do not gain plaintext USB logging.
|
||||
build-companion-firmwares-logging-matrix: Build canonical Companion targets with merged runtime USB logging where the transport is safe, plus applicable MQTT and expanded FULL profiles. Full Companion replaces separate USB, BLE, WiFi, Terminal Chat, and USB-logging artifacts where an exact combined recipe exists.
|
||||
build-full-esp32-firmwares: Build feature-complete ESP32 profiles with up to 254 neighbors, USB packet logging, WiFi MQTT plus ESP-NOW where a matching MQTT recipe exists, LoRa OTA, and expanded dual-OTA partitions.
|
||||
build-full-esp32-logging-firmwares: Build only the FULL USB-logging fallback for targets without a matching WiFi MQTT environment.
|
||||
@@ -132,13 +132,13 @@ Commands:
|
||||
build-kiss-radio-firmwares: Build all KISS radio firmwares for all build targets.
|
||||
get-companion-firmwares-to-build: List canonical attached companion targets for release automation; Full Companion replaces separate transport artifacts where qualified.
|
||||
get-repeater-firmwares-to-build: List canonical, specialized external-storage, and deployed-target OTA compatibility repeaters for release automation.
|
||||
get-room-server-firmwares-to-build: List standard room-server targets for release automation.
|
||||
get-room-server-firmwares-to-build: List standard and portable LoRa-OTA room-server targets for release automation.
|
||||
|
||||
Options:
|
||||
--firmware-version <version>: Firmware version to embed.
|
||||
--radio-preset <name|number>: Override the USA Cascadia radio default. Stable names are usa-cascadia and target; legacy menu numbers remain accepted.
|
||||
--profile <default|cascade>: Override runtime settings embedded in the firmware (not its feature set).
|
||||
--build-profile <auto|standard|full>: Select feature/partition policy. Auto uses the combined Full MQTT/USB/WiFi recipe when it covers the plain infrastructure target; otherwise it first builds complete LoRa-OTA-capable firmware with a measured-size fallback. Internal-flash nRF52 repeaters also publish the reduced image for delta-staging headroom. Standard preserves portable images, including the 1.25 MiB slot; full requires the expanded ESP32 recipe.
|
||||
--build-profile <auto|standard|full>: Select feature/partition policy. Auto uses the combined Full MQTT/USB/WiFi recipe when it covers the plain infrastructure target; otherwise it first builds complete LoRa-OTA-capable firmware with a measured-size fallback. Internal-flash nRF52 repeaters and room servers also publish the reduced image for delta-staging headroom. Standard preserves portable images, including the 1.25 MiB slot; full requires the expanded ESP32 recipe.
|
||||
--auto|--standard|--full: Short forms of --build-profile.
|
||||
--skip-kiss|--include-kiss: Exclude (default) or include KISS modem targets in bulk builds.
|
||||
--clean|--resume: Clean output or resume existing Option 3/FULL-only artifacts.
|
||||
@@ -334,11 +334,11 @@ for section, options in data:
|
||||
' "$SUPPORTED_PLATFORM_PATTERN" <<<"$PIO_CONFIG_JSON"
|
||||
)
|
||||
|
||||
# Keep each ordinary repeater environment feature-rich (including external
|
||||
# sensors), and expose a separately named no-external-sensors OTA build for
|
||||
# ESP32/nRF52 repeaters that need a lean staging profile. These two platforms
|
||||
# have a complete apply path; RP2040 and STM32 do not yet have the required
|
||||
# bootloader/apply path.
|
||||
# Keep each ordinary repeater and room-server environment feature-rich
|
||||
# (including external sensors), and expose a separately named
|
||||
# no-external-sensors OTA build for ESP32/nRF52 infrastructure roles that
|
||||
# need a lean staging profile. These two platforms have a complete apply
|
||||
# path; RP2040 and STM32 do not yet have the required bootloader/apply path.
|
||||
local env_name ota_env full_env usb_env ble_env
|
||||
local -a base_envs=("${SUPPORTED_PIO_ENVS[@]}")
|
||||
for env_name in "${base_envs[@]}"; do
|
||||
@@ -347,17 +347,18 @@ for section, options in data:
|
||||
*) continue ;;
|
||||
esac
|
||||
# Generate one lean LoRa-OTA image for each board's standalone repeater
|
||||
# role. Observer, Ethernet, and bridge profiles are separate roles; any
|
||||
# purpose-built OTA versions of those remain explicit PlatformIO targets.
|
||||
# or room-server role. Sensor, observer, Ethernet, and bridge profiles
|
||||
# stay on their explicit PlatformIO recipes.
|
||||
case "${env_name,,}" in
|
||||
*_repeater|*_repeater_|*_repeatr|*_repeatr_) ;;
|
||||
*_repeater|*_repeater_|*_repeatr|*_repeatr_|*_room_server|*_room_server_|*_room_svr|*_room_svr_) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
# SolarXiao 30S/33S repeaters already use matched external QSPI staging,
|
||||
# so their normal full-sensor image is install-capable. A second lean
|
||||
# no-external-sensors target provides no additional OTA capability.
|
||||
# SolarXiao 30S/33S repeater and room-server roles already use matched
|
||||
# external QSPI staging, so their normal full-sensor image is
|
||||
# install-capable. A second lean no-external-sensors target provides no
|
||||
# additional OTA capability.
|
||||
case "${env_name,,}" in
|
||||
solarxiao_30s_repeater|solarxiao_33s_repeater) continue ;;
|
||||
solarxiao_30s_repeater|solarxiao_33s_repeater|solarxiao_30s_room_server|solarxiao_33s_room_server) continue ;;
|
||||
esac
|
||||
ota_env="${env_name%_}_lora_ota_no_external_sensors"
|
||||
PIO_ENV_COMPLETE_OTA_BASE_BY_NAME["$ota_env"]="$env_name"
|
||||
@@ -595,6 +596,9 @@ canonicalize_variant_suffix() {
|
||||
companion_ble|companion_radio_ble)
|
||||
echo "companion_radio_ble"
|
||||
;;
|
||||
room_svr_lora_ota_no_external_sensors|room_server_lora_ota_no_external_sensors)
|
||||
echo "room_server_lora_ota_no_external_sensors"
|
||||
;;
|
||||
room_svr|room_server)
|
||||
echo "room_server"
|
||||
;;
|
||||
@@ -1976,6 +1980,12 @@ print_release_firmware_targets() {
|
||||
;;
|
||||
get-room-server-firmwares-to-build)
|
||||
get_pio_envs_ending_with_string "_room_server"
|
||||
for env_name in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
if is_room_server_role_target "$env_name" \
|
||||
&& is_lora_ota_no_external_sensors_target "$env_name"; then
|
||||
printf '%s\n' "$env_name"
|
||||
fi
|
||||
done
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
@@ -2884,7 +2894,8 @@ get_reduced_lora_ota_target() {
|
||||
echo "$target"
|
||||
return 0
|
||||
fi
|
||||
if ! is_repeater_role_target "$target"; then
|
||||
if ! is_repeater_role_target "$target" \
|
||||
&& ! is_room_server_role_target "$target"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -2927,10 +2938,10 @@ is_lora_ota_build() {
|
||||
|
||||
# A single-target auto build promises to preserve the capabilities declared
|
||||
# by its resolved PlatformIO environment and fail if they do not fit. Keep
|
||||
# that contract for nRF52 Companion and other non-repeater roles which
|
||||
# deliberately inherit the shared OTA recipe. The standard/release profile
|
||||
# continues through the opt-in policy below and may select its documented
|
||||
# portable reductions.
|
||||
# that contract for nRF52 Companion and roles other than standalone repeaters
|
||||
# and room servers which deliberately inherit the shared OTA recipe. The
|
||||
# standard/release profile continues through the opt-in policy below and may
|
||||
# select its documented portable reductions.
|
||||
if [ "${BUILD_PROFILE_FOR_TARGET:-$BUILD_PROFILE_EFFECTIVE}" = "auto" ]; then
|
||||
return 0
|
||||
fi
|
||||
@@ -2951,12 +2962,12 @@ is_lora_ota_build() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The constrained portable OTA profile is only emitted for repeaters. FULL
|
||||
# ESP32 builds returned above also enable LoRa OTA for room-server, sensor,
|
||||
# observer, and bridge roles because their expanded slots have room for every
|
||||
# feature.
|
||||
# The constrained portable OTA profile is emitted for standalone repeaters
|
||||
# and room servers. FULL ESP32 builds returned above also enable LoRa OTA for
|
||||
# sensor, observer, and bridge roles because their expanded slots have room
|
||||
# for every feature.
|
||||
case "$env_name_lc" in
|
||||
*repeater*|*repeatr*) return 0 ;;
|
||||
*repeater*|*repeatr*|*room_server*|*room_svr*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
@@ -6153,7 +6164,7 @@ run_auto_two_pass_build() {
|
||||
echo "Auto pass 1 failed for a non-size reason; refusing to hide the compiler or capability failure with a reduced build."
|
||||
elif [ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = "1" ] \
|
||||
&& [ -n "$fallback_target" ]; then
|
||||
echo "Auto pass 1 succeeded. Internal-flash nRF52 repeater policy also publishes a reduced LoRa OTA image with more delta-staging headroom."
|
||||
echo "Auto pass 1 succeeded. Internal-flash nRF52 repeater/room-server policy also publishes a reduced LoRa OTA image with more delta-staging headroom."
|
||||
ESP32_FULL_BUILD=0
|
||||
BUILD_PROFILE_EFFECTIVE="standard"
|
||||
FIRMWARE_FILENAME_INFIX="reduced-ota"
|
||||
|
||||
@@ -28,7 +28,7 @@ and partition policy:
|
||||
|
||||
| Selection | Behavior |
|
||||
| --- | --- |
|
||||
| `auto` | For one explicit target, pass 1 builds the complete supported LoRa-OTA-capable recipe. ESP32 boards with a qualified expanded profile use it; other repeaters attempt the complete recipe in their current application region. A non-repeater keeps every capability declared by its resolved PlatformIO recipe, including LoRa OTA, and fails instead of silently removing one that does not fit. A measured repeater flash/partition overflow starts the standard `no_external_sensors` LoRa OTA pass. Internal-flash nRF52 repeaters publish that reduced pass even when the complete image fits, because the smaller running image leaves more room to stage a delta; matched QSPI/SD repeaters do not need the redundant artifact. Compiler errors and missing-capability checks never trigger or conceal a reduced build. Canonical bulk commands keep their established standard partition contract. |
|
||||
| `auto` | For one explicit target, pass 1 builds the complete supported LoRa-OTA-capable recipe. ESP32 boards with a qualified expanded profile use it; other standalone repeaters and room servers attempt the complete recipe in their current application region. Other roles keep every capability declared by their resolved PlatformIO recipe, including LoRa OTA, and fail instead of silently removing one that does not fit. A measured repeater or room-server flash/partition overflow starts the standard `no_external_sensors` LoRa OTA pass. Internal-flash nRF52 repeaters and room servers publish that reduced pass even when the complete image fits, because the smaller running image leaves more room to stage a delta; matched QSPI/SD roles do not need the redundant artifact. Compiler errors and missing-capability checks never trigger or conceal a reduced build. Canonical bulk commands keep their established standard partition contract. |
|
||||
| `standard` | Immediately uses the deployed/portable partition contract and its documented reductions. This is useful when the operator already knows the expanded or complete image is unsuitable. |
|
||||
| `full` | Requires a qualified ESP32 expanded-partition target (or an explicitly named Full Companion). Install a matching merged image when this changes the partition table. |
|
||||
|
||||
@@ -60,6 +60,10 @@ canonical Full image under the normal target identity. Install its matching
|
||||
merged image over USB once; mOTA rejects the incompatible partition signature
|
||||
until that migration is complete, then later Full updates use the normal
|
||||
target ID. The portable image remains available while deployed nodes move.
|
||||
|
||||
Legacy supported ESP32 Wi-Fi repeaters can use the
|
||||
[Wi-Fi partition migration](esp32_wifi_partition_migration.md) bridge to
|
||||
preserve their identity and install an expanded Full image without a USB flash.
|
||||
The default `cascade` settings profile enables device and LoRa RX power saving;
|
||||
saved preferences take precedence, and active USB/network services can prevent
|
||||
device sleep. `--profile default` instead uses the upstream power-saving defaults.
|
||||
@@ -96,7 +100,7 @@ getters; relaying a command over LoRa keeps its remote restrictions.
|
||||
| Standard non-MQTT repeater or room server | Keeps the normal role CLI and, where USB is a safe plaintext console, embeds debug/packet logging behind persistent `get/set usb.logging`. The explicitly selected portable policy can omit WebConfig and browser WiFi OTA, so those commands are unavailable and the omission is recorded in the capability manifest. |
|
||||
| Legacy standard logging | No longer emitted separately. Its behavior is compiled into the ordinary artifact. Size-constrained STM32 targets embed packet logging without verbose `MESH_DEBUG`. |
|
||||
| LoRa-OTA (`-ota-`) | LoRa OTA adds the `ota ...` commands; it does not otherwise reduce the role CLI. ESP32 `no_external_sensors` artifacts retain the compact browser WiFi uploader, the complete CLI, and up to 254 neighbors, subject to recorded internal-DRAM reductions. |
|
||||
| Internal-flash nRF52 repeater auto pair | `full-ota` retains the board's external-sensor drivers; `reduced-ota` omits the declared optional sensors to leave additional internal-flash staging room. RAK3401 and RAK4631 reduced builds retain INA219, INA226, INA260, and INA3221 I2C voltage/current monitors at a measured cost below 5 KiB. Both artifacts carry the same stable OTA target identity and are checked for `ota ...` and `retry.preset`; RAK artifacts also verify the retained monitor drivers. |
|
||||
| Internal-flash nRF52 repeater/room-server auto pair | `full-ota` retains the board's external-sensor drivers; `reduced-ota` omits the declared optional sensors to leave additional internal-flash staging room. RAK3401 and RAK4631 reduced builds retain INA219, INA226, INA260, and INA3221 I2C voltage/current monitors at a measured cost below 5 KiB. Both artifacts carry the same stable OTA target identity and are checked for `ota ...` and `retry.preset`; RAK artifacts also verify the retained monitor drivers. |
|
||||
| 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 USB + WiFi | Uses the matching MQTT target with packet logging on, verbose debug off, and the complete command surface supported by that role and hardware. `get/set logging.output off\|usb\|wifi\|both` selects and persists the active output paths. |
|
||||
| FULL ESP32 logging fallback | Uses the matching non-MQTT target only when no WiFi MQTT sibling exists, with debug and packet logging enabled and the complete command surface supported by that role and hardware. Its persistent USB gate also covers output-off operation, avoiding a second FULL ESP-NOW image. |
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# ESP32 legacy-partition migration over Wi-Fi
|
||||
|
||||
The partition-migration bridge lets a supported ESP32 node move from a small
|
||||
dual-OTA layout to a verified larger dual-OTA layout without cable flashing.
|
||||
It is designed for inaccessible repeaters: the bridge itself fits a legacy
|
||||
1.25 MiB slot, replaces the table from Wi-Fi, and then exposes Wi-Fi again for
|
||||
the full application image.
|
||||
|
||||
It currently has explicit, byte-for-byte generated target plans for Arduino
|
||||
ESP32 `default_8MB.csv` and `default_16MB.csv`. It does not guess a table for
|
||||
other flash sizes. Adding a board later means adding one reviewed target plan
|
||||
and its generated table-prefix test; the bridge core is otherwise shared.
|
||||
|
||||
| Flash size | Target table | app0 / app1 size | Typical board |
|
||||
| --- | --- | --- | --- |
|
||||
| 8 MiB | `default_8MB.csv` | 0x330000 / 0x330000 | Seeed XIAO ESP32-S3 |
|
||||
| 16 MiB | `default_16MB.csv` | 0x640000 / 0x640000 | Heltec V4 / V4.3 OLED |
|
||||
|
||||
## What the bridge preserves
|
||||
|
||||
Before it touches the partition table, the bridge reads the historical
|
||||
`/identity/_main.id` SPIFFS file. It requires all 96 bytes (32-byte public key
|
||||
followed by its 64-byte private key), stages them in an NVS namespace that is
|
||||
unchanged by both target layouts, then changes the table. On the first boot
|
||||
with the expanded layout it writes the staged value into SPIFFS, reads it back
|
||||
byte-for-byte, and only then clears the NVS staging record and offers the final
|
||||
uploader.
|
||||
|
||||
The private identity is the guaranteed preservation boundary. The old SPIFFS
|
||||
image is deliberately not raw-copied into an expanded partition: SPIFFS is not
|
||||
safe to resize that way. Other SPIFFS-backed settings can be recreated after
|
||||
the full image boots.
|
||||
|
||||
## Eligible source layout
|
||||
|
||||
The bridge only accepts a source table that has all of the following:
|
||||
|
||||
- NVS at `0x9000`, size `0x5000`, and OTA metadata at `0xE000`, size `0x2000`.
|
||||
- Two distinct, non-empty OTA application slots and a non-empty SPIFFS
|
||||
partition, all inside the physical flash size.
|
||||
- A known exact 8 MiB or 16 MiB physical flash capacity and a source table
|
||||
different from that capacity's target table.
|
||||
|
||||
This means the source app-slot and SPIFFS sizes can vary; the bridge validates
|
||||
the live geometry instead of carrying board-name rules. It also ensures it is
|
||||
running from one source OTA slot. If it arrived in B, it CRC-copies itself to
|
||||
the future A address, selects A in OTA metadata, writes the table, and
|
||||
restarts. It therefore works whether the browser OTA updater placed the bridge
|
||||
in legacy A or B.
|
||||
|
||||
Keep power stable while the bridge is replacing the 4 KiB table sector. A loss
|
||||
of power before that point leaves the old table and source intact. After that
|
||||
small critical write, the selected A copy of the bridge is the recovery path.
|
||||
|
||||
## Build and migrate a Seeed XIAO ESP32-S3
|
||||
|
||||
The production bridge is a legacy-slot application image:
|
||||
|
||||
```powershell
|
||||
& "$env:USERPROFILE\.platformio\penv\Scripts\pio.exe" run -e xiao_s3_partition_migrator
|
||||
```
|
||||
|
||||
Build the Full XIAO image with its 8 MiB table:
|
||||
|
||||
```powershell
|
||||
$env:MESHCORE_ESP32_FULL_BUILD = '1'
|
||||
& "$env:USERPROFILE\.platformio\penv\Scripts\pio.exe" run -e Xiao_S3_WIO_repeater_observer_mqtt
|
||||
```
|
||||
|
||||
Upload the bridge through the legacy node's existing Wi-Fi browser updater.
|
||||
Then join `MeshCore-Migrate` (password `meshcore-migrate`) and wait for
|
||||
**Expanded layout ready**. Upload the Full app-only `.bin` from the second
|
||||
build at `/update`. Do not upload an `-merged.bin`: merged images include
|
||||
bootloader and table offsets for cable flashing, not browser OTA.
|
||||
|
||||
`xiao_s3_partition_legacy_seed` is a disposable-hardware test image only. It
|
||||
models the legacy table and an identity file; it is not a deployable repeater.
|
||||
|
||||
## Heltec V4 / V4.3
|
||||
|
||||
Build `heltec_v4_partition_migrator`, upload it through the legacy Wi-Fi
|
||||
updater, then upload the Full V4 app-only image after the bridge reports ready.
|
||||
The 16 MiB target table creates two 6.25 MiB OTA slots. See the
|
||||
[V4 build notes](heltec_v4_wifi_partition_migration.md) for its build command.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Heltec V4 legacy-partition migration over Wi-Fi
|
||||
|
||||
Heltec V4/V4.3 OLED repeaters with their normal 16 MiB flash use the shared
|
||||
[ESP32 Wi-Fi partition migration](esp32_wifi_partition_migration.md) bridge.
|
||||
It changes Arduino's old `default.csv` layout, with two 1.25 MiB OTA slots,
|
||||
to `default_16MB.csv`, with two 6.25 MiB OTA slots.
|
||||
|
||||
Build the bridge application image:
|
||||
|
||||
```powershell
|
||||
& "$env:USERPROFILE\.platformio\penv\Scripts\pio.exe" run -e heltec_v4_partition_migrator
|
||||
```
|
||||
|
||||
Upload `.pio/build/heltec_v4_partition_migrator/firmware.bin` through the
|
||||
legacy browser Wi-Fi updater. Rejoin `MeshCore-Migrate` with password
|
||||
`meshcore-migrate`; when it reports **Expanded layout ready**, upload the
|
||||
app-only Full V4 repeater image at `/update`.
|
||||
|
||||
Do not use an `-merged.bin` in either browser uploader. Merged images contain
|
||||
bootloader and table offsets for cable flashing. The bridge preserves the
|
||||
private identity; other SPIFFS-backed settings may be recreated after the
|
||||
Full image starts.
|
||||
+7
-6
@@ -82,7 +82,7 @@ Both paths require:
|
||||
|
||||
- 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 in the normal full-sensor repeater or room-server 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
|
||||
@@ -94,8 +94,9 @@ 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 standalone ESP32 and nRF52 repeater
|
||||
targets that need a smaller internal update workspace. Those siblings omit selected optional environmental
|
||||
`build.sh` provides a `*_repeater_lora_ota_no_external_sensors` or
|
||||
`*_room_server_lora_ota_no_external_sensors` build for standalone ESP32 and nRF52
|
||||
infrastructure targets that need a smaller internal update workspace. Those siblings omit selected optional environmental
|
||||
and ranging drivers while retaining board-native features such as displays, buttons, battery monitoring,
|
||||
and GPS where the target uses the GPS-preserving lean profile. RAK3401 and RAK4631 reduced builds also
|
||||
retain INA219, INA226, INA260, and INA3221 I2C voltage/current monitors; together these drivers cost less
|
||||
@@ -116,7 +117,7 @@ bus at those addresses. Keep RAK12500 at `0x42`, strap INA3221 A0 to SCL for `0x
|
||||
|
||||
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
|
||||
repeater or room-server install-capable; those targets do not need to reserve internal flash
|
||||
for the downloaded container. SolarXiao 30S and 33S use this matched external-QSPI
|
||||
path and therefore do not emit redundant no-external-sensors siblings. A
|
||||
RAK19007 can use the same separately wired W25Q16 with either RAK4631 or
|
||||
@@ -131,13 +132,13 @@ as recorded in their capability manifests. 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.
|
||||
|
||||
For one explicitly selected internal-flash nRF52 repeater, the default
|
||||
For one explicitly selected internal-flash nRF52 repeater or room server, the default
|
||||
`--build-profile auto` run emits two install-capable choices. The
|
||||
`*-full-ota-*` artifact retains external sensors, while the
|
||||
`*_lora_ota_no_external_sensors-reduced-ota-*` artifact trades only those
|
||||
optional drivers not explicitly retained by the board profile for more delta-staging space. They retain the same OTA target
|
||||
identity so update tooling does not confuse the optimization choice with a
|
||||
different board. QSPI- and SD-backed repeaters emit only the complete choice
|
||||
different board. QSPI- and SD-backed repeater and room-server roles emit only the complete choice
|
||||
unless a measured size failure requires a fallback.
|
||||
|
||||
nRF52 `-ota-` siblings are compiled with size optimization instead of the Adafruit platform's default
|
||||
|
||||
+12
-10
@@ -133,12 +133,13 @@ artifacts, including room, sensor, and repeater roles, must fit the legacy slot
|
||||
limit and the target's actual app partition. The ESP32-C6 `no_external_sensors` OTA siblings are the narrow
|
||||
exception: the Arduino 3.x WiFi runtime cannot fit that cross-family ceiling, so those images retain their
|
||||
established target-specific 1920 KiB or larger A/B app layout and are checked against the actual app
|
||||
partition. For standalone ESP32 and nRF52 repeaters that need a lean staging
|
||||
profile, `build.sh` also exposes an explicit `*_lora_ota_no_external_sensors`
|
||||
artifact: the ordinary repeater remains sensor-enabled, while that sibling
|
||||
trims selected optional environmental/ranging drivers for LoRa distribution. SolarXiao 30S and 33S use
|
||||
matched external QSPI staging, so their ordinary full-sensor repeater is already install-capable and no
|
||||
redundant lean sibling is generated. Integrated GPS and other
|
||||
partition. For standalone ESP32 and nRF52 repeater and room-server roles that
|
||||
need a lean staging profile, `build.sh` also exposes an explicit
|
||||
`*_lora_ota_no_external_sensors` artifact: the ordinary role remains
|
||||
sensor-enabled, while that sibling trims selected optional environmental/ranging
|
||||
drivers for LoRa distribution. SolarXiao 30S and 33S use matched external QSPI
|
||||
staging, so their ordinary full-sensor repeater and room-server images are
|
||||
already install-capable and no redundant lean sibling is generated. Integrated GPS and other
|
||||
board-native telemetry remain enabled where the target selects the GPS-preserving lean profile. The legacy
|
||||
suffix describes a driver trim, not removal of the generic I2C bus. Reduced RAK3401 and RAK4631 profiles
|
||||
retain INA219, INA226, INA260, and INA3221 voltage/current monitors. These are retained optional-sensor-table
|
||||
@@ -167,9 +168,10 @@ WiFi-heavy non-companion roles are not reduced to fit the legacy application slo
|
||||
promotes every ESP32 MQTT observer and ESP-NOW bridge to the expanded FULL partition profile. These artifacts
|
||||
retain the complete role CLI, WebConfig where supported, display and optional sensor support, full timezone
|
||||
and TLS behavior, and the board's normal power-management implementation. The compact CLI is not compiled
|
||||
into any build. Ordinary repeater builds remain sensor-enabled; only explicitly named
|
||||
`*_lora_ota_no_external_sensors` siblings trim selected optional environmental/ranging drivers for LoRa
|
||||
distribution, and those siblings retain the complete CLI and target-declared I2C peripherals.
|
||||
into any build. Ordinary repeater and room-server builds remain sensor-enabled;
|
||||
only explicitly named `*_lora_ota_no_external_sensors` siblings trim selected
|
||||
optional environmental/ranging drivers for LoRa distribution, and those
|
||||
siblings retain the complete CLI and target-declared I2C peripherals.
|
||||
|
||||
MQTT observer radio and bridge preferences use verified temporary files plus a recoverable backup. A reset
|
||||
during a settings save restores the last committed common preference image or publishes the completed new
|
||||
@@ -192,7 +194,7 @@ full ElegantOTA where that target declares the required library, and LoRa OTA fo
|
||||
including room servers, sensors, observers, and bridges. They use expanded A/B partition
|
||||
tables: 1984 KiB application slots on 4 MiB boards and the framework's larger dual-OTA tables on 8 MiB
|
||||
and 16 MiB boards. Explicit `*_lora_ota_no_external_sensors` targets are not duplicated; their ordinary
|
||||
repeater build is the FULL, sensor-enabled counterpart. The
|
||||
repeater or room-server build is the FULL, sensor-enabled counterpart. The
|
||||
`*-full-usb-wifi-ota-*` profile enables USB packet logging and MQTT, with a
|
||||
persistent `logging.output` selector; its verbose internal debug remains off.
|
||||
The fallback `*-full-logging-ota-*` profile enables USB debug and packet
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Disposable-hardware test seed for the ESP32 Wi-Fi partition migrator.
|
||||
//
|
||||
// This is intentionally not a deployable MeshCore role. It models the two
|
||||
// properties that matter before an inaccessible legacy node is migrated: an
|
||||
// ordinary 1.25 MiB Arduino OTA partition table and the historical 96-byte
|
||||
// MeshCore identity file. The keypair below is MeshCore's known-valid test
|
||||
// identity; it makes a full production image able to load the preserved key
|
||||
// after the migration without exposing a real user's private key.
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncElegantOTA.h>
|
||||
#include <SPIFFS.h>
|
||||
|
||||
#include <helpers/esp32/WiFiRadioPolicy.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kApSsid[] = "MeshCore-Legacy-Seed";
|
||||
constexpr char kApPassword[] = "meshcore-seed";
|
||||
constexpr char kIdentityPath[] = "/identity/_main.id";
|
||||
|
||||
// File order is historical MeshCore order: public key first, then private key.
|
||||
constexpr uint8_t kIdentity[96] = {
|
||||
0x1e, 0xc7, 0x71, 0x75, 0xb0, 0x91, 0x8e, 0xd2,
|
||||
0x06, 0xf9, 0xae, 0x04, 0xec, 0x13, 0x6d, 0x6d,
|
||||
0x5d, 0x43, 0x15, 0xbb, 0x26, 0x30, 0x54, 0x27,
|
||||
0xf6, 0x45, 0xb4, 0x92, 0xe9, 0x35, 0x0c, 0x10,
|
||||
0x70, 0x65, 0xe1, 0x8f, 0xd9, 0xfa, 0xbb, 0x70,
|
||||
0xc1, 0xed, 0x90, 0xdc, 0xa1, 0x99, 0x07, 0xde,
|
||||
0x69, 0x8c, 0x88, 0xb7, 0x09, 0xea, 0x14, 0x6e,
|
||||
0xaf, 0xd9, 0x3d, 0x9b, 0x83, 0x0c, 0x7b, 0x60,
|
||||
0xc4, 0x68, 0x11, 0x93, 0xc7, 0x9b, 0xbc, 0x39,
|
||||
0x94, 0x5b, 0xa8, 0x06, 0x41, 0x04, 0xbb, 0x61,
|
||||
0x8f, 0x8f, 0xd7, 0xa8, 0x4a, 0x0a, 0xf6, 0xf5,
|
||||
0x70, 0x33, 0xd6, 0xe8, 0xdd, 0xcd, 0x64, 0x71,
|
||||
};
|
||||
|
||||
AsyncWebServer server(80);
|
||||
|
||||
bool installIdentity() {
|
||||
if (!SPIFFS.begin(true)) return false;
|
||||
if (!SPIFFS.exists("/identity") && !SPIFFS.mkdir("/identity")) {
|
||||
SPIFFS.end();
|
||||
return false;
|
||||
}
|
||||
File existing = SPIFFS.open(kIdentityPath, "r");
|
||||
bool already_valid = existing && existing.size() == sizeof(kIdentity);
|
||||
if (already_valid) {
|
||||
uint8_t check[sizeof(kIdentity)];
|
||||
already_valid = existing.read(check, sizeof(check)) == sizeof(check)
|
||||
&& memcmp(check, kIdentity, sizeof(check)) == 0;
|
||||
}
|
||||
if (existing) existing.close();
|
||||
if (!already_valid) {
|
||||
File file = SPIFFS.open(kIdentityPath, "w");
|
||||
const bool wrote = file && file.write(kIdentity, sizeof(kIdentity)) == sizeof(kIdentity);
|
||||
if (file) {
|
||||
file.flush();
|
||||
file.close();
|
||||
}
|
||||
if (!wrote) {
|
||||
SPIFFS.end();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
SPIFFS.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
void startAccessPoint() {
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
WiFi.setSleep(false);
|
||||
const IPAddress address(192, 168, 4, 1);
|
||||
const IPAddress netmask(255, 255, 255, 0);
|
||||
if (!WiFi.softAPConfig(address, address, netmask)
|
||||
|| !WiFi.softAP(kApSsid, kApPassword, mesh::wifi::accessPointChannel())
|
||||
|| mesh::wifi::applyAccessPointProtocolMask() != ESP_OK
|
||||
|| esp_wifi_set_protocol(WIFI_IF_STA, mesh::wifi::kProtocolMask) != ESP_OK
|
||||
|| esp_wifi_set_max_tx_power(78) != ESP_OK) {
|
||||
Serial.println("Legacy seed AP failed");
|
||||
return;
|
||||
}
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
request->send(200, "text/html",
|
||||
"<!doctype html><meta name=viewport content='width=device-width,initial-scale=1'>"
|
||||
"<h2>MeshCore legacy OTA seed</h2>"
|
||||
"<p>1.25 MiB A/B layout with a valid 96-byte identity file.</p>"
|
||||
"<p><a href='/update'>Upload the partition migrator</a></p>");
|
||||
});
|
||||
AsyncElegantOTA.begin(&server);
|
||||
server.begin();
|
||||
Serial.printf("Legacy seed ready. Join %s and open http://192.168.4.1/\n", kApSsid);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(250);
|
||||
const bool identity_ok = installIdentity();
|
||||
Serial.printf("Legacy seed identity: %s, public key "
|
||||
"1EC77175B0918ED206F9AE04EC136D6D5D4315BB26305427F645B492E9350C10\n",
|
||||
identity_ok ? "ready" : "FAILED");
|
||||
if (identity_ok) startAccessPoint();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(20);
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncElegantOTA.h>
|
||||
#include <Preferences.h>
|
||||
#include <SPIFFS.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_attr.h>
|
||||
#include <esp_flash.h>
|
||||
#include <esp_partition.h>
|
||||
|
||||
#include <helpers/ESP32PartitionMigrationPolicy.h>
|
||||
#include <helpers/esp32/WiFiRadioPolicy.h>
|
||||
|
||||
namespace migration = mesh::esp32_partition_migration;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kApSsid[] = "MeshCore-Migrate";
|
||||
constexpr char kApPassword[] = "meshcore-migrate";
|
||||
#ifndef MESHCORE_MIGRATION_DELAY_MS
|
||||
#define MESHCORE_MIGRATION_DELAY_MS 4500
|
||||
#endif
|
||||
#ifndef MESHCORE_MIGRATION_RESTART_DELAY_MS
|
||||
#define MESHCORE_MIGRATION_RESTART_DELAY_MS 250
|
||||
#endif
|
||||
constexpr uint32_t kMigrationDelayMs = MESHCORE_MIGRATION_DELAY_MS;
|
||||
constexpr uint32_t kMigrationRestartDelayMs = MESHCORE_MIGRATION_RESTART_DELAY_MS;
|
||||
constexpr size_t kCopyBufferBytes = 4096;
|
||||
constexpr size_t kIdentityFileBytes = 96; // public key (32) followed by private key (64)
|
||||
constexpr char kMigrationNvsNamespace[] = "mesh-pt-migrate";
|
||||
constexpr char kMigrationIdentityKey[] = "identity";
|
||||
// NVS key names are limited to 15 characters.
|
||||
constexpr char kMigrationIdentityPendingKey[] = "id-pending";
|
||||
static_assert(sizeof(kMigrationNvsNamespace) - 1 <= 15,
|
||||
"ESP32 NVS namespace names are limited to 15 characters");
|
||||
|
||||
AsyncWebServer server(80);
|
||||
bool migration_started = false;
|
||||
bool migration_complete = false;
|
||||
bool reboot_requested = false;
|
||||
uint32_t migration_at = 0;
|
||||
uint32_t reboot_at = 0;
|
||||
char status_text[160] = "Starting";
|
||||
|
||||
// The Arduino loop task can use external RAM on an ESP32-S3. Flash
|
||||
// erase/write disables the external-memory cache, so every buffer passed to a
|
||||
// flash operation must be explicitly placed in internal DRAM.
|
||||
DRAM_ATTR uint8_t copy_buffer[kCopyBufferBytes];
|
||||
DRAM_ATTR uint8_t partition_table_bytes[
|
||||
migration::kExpandedPartitionTablePrefixBytes];
|
||||
DRAM_ATTR uint8_t partition_table_verified[
|
||||
migration::kExpandedPartitionTablePrefixBytes];
|
||||
// Keep the temporary flash hooks in DRAM. The normal ESP-IDF hooks keep the
|
||||
// two CPU cores and their flash caches safe while an erase/write is running;
|
||||
// only the address-validation callback is narrowed for this one deliberately
|
||||
// dangerous sector. esp_flash_os_functions_t is documented for advanced
|
||||
// callers which need to replace individual hooks.
|
||||
DRAM_ATTR esp_flash_os_functions_t partition_table_flash_hooks;
|
||||
using RegionProtectedFn = esp_err_t (*)(void*, size_t, size_t);
|
||||
DRAM_ATTR RegionProtectedFn original_region_protected = nullptr;
|
||||
DRAM_ATTR void* original_flash_hook_data = nullptr;
|
||||
// esp_partition_write() reads the descriptor while the flash cache is off.
|
||||
// Keep the synthetic copy destination out of a task stack that may be
|
||||
// allocated in PSRAM on ESP32-S3 builds.
|
||||
DRAM_ATTR esp_partition_t copy_destination;
|
||||
|
||||
struct PartitionRefs {
|
||||
const esp_partition_t* nvs = nullptr;
|
||||
const esp_partition_t* otadata = nullptr;
|
||||
const esp_partition_t* app0 = nullptr;
|
||||
const esp_partition_t* app1 = nullptr;
|
||||
const esp_partition_t* spiffs = nullptr;
|
||||
};
|
||||
|
||||
uint32_t crc32(const uint8_t* data, size_t size, uint32_t crc = 0xFFFFFFFFU) {
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
crc ^= data[i];
|
||||
for (unsigned bit = 0; bit < 8; ++bit) {
|
||||
crc = (crc >> 1) ^ ((crc & 1U) ? 0xEDB88320U : 0U);
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
// This callback is invoked before ESP-IDF enters its cache-off flash critical
|
||||
// section. It permits only the partition-table sector, and delegates every
|
||||
// other address to ESP-IDF's original protection policy.
|
||||
esp_err_t IRAM_ATTR partitionTableRegionProtected(void*, size_t address, size_t size) {
|
||||
const size_t table_start = migration::kPartitionTableAddress;
|
||||
const size_t table_end = table_start + migration::kPartitionTableBytes;
|
||||
if (address >= table_start && size <= table_end - address) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (!original_region_protected) return ESP_ERR_INVALID_STATE;
|
||||
return original_region_protected(original_flash_hook_data, address, size);
|
||||
}
|
||||
|
||||
const char* errName(esp_err_t err) {
|
||||
const char* name = esp_err_to_name(err);
|
||||
return name ? name : "unknown ESP error";
|
||||
}
|
||||
|
||||
bool findPartitions(PartitionRefs& refs, migration::PartitionGeometry& geometry) {
|
||||
refs.nvs = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
||||
ESP_PARTITION_SUBTYPE_DATA_NVS, nullptr);
|
||||
refs.otadata = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
||||
ESP_PARTITION_SUBTYPE_DATA_OTA, nullptr);
|
||||
refs.app0 = esp_partition_find_first(ESP_PARTITION_TYPE_APP,
|
||||
ESP_PARTITION_SUBTYPE_APP_OTA_0, nullptr);
|
||||
refs.app1 = esp_partition_find_first(ESP_PARTITION_TYPE_APP,
|
||||
ESP_PARTITION_SUBTYPE_APP_OTA_1, nullptr);
|
||||
refs.spiffs = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
||||
ESP_PARTITION_SUBTYPE_DATA_SPIFFS, nullptr);
|
||||
if (!refs.nvs || !refs.otadata || !refs.app0 || !refs.app1 || !refs.spiffs) {
|
||||
return false;
|
||||
}
|
||||
geometry = {
|
||||
refs.nvs->address, refs.nvs->size,
|
||||
refs.otadata->address, refs.otadata->size,
|
||||
refs.app0->address, refs.app0->size,
|
||||
refs.app1->address, refs.app1->size,
|
||||
refs.spiffs->address, refs.spiffs->size,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
// esp_partition_* validates only the geometry in the supplied descriptor. A
|
||||
// descriptor for the future data region is therefore enough to write it while
|
||||
// the old partition table is still active. This avoids touching old SPIFFS
|
||||
// until the target table is published.
|
||||
esp_partition_t rawPartition(uint32_t address, uint32_t size, const char* label) {
|
||||
esp_partition_t part = {};
|
||||
part.type = ESP_PARTITION_TYPE_DATA;
|
||||
part.subtype = ESP_PARTITION_SUBTYPE_DATA_UNDEFINED;
|
||||
part.address = address;
|
||||
part.size = size;
|
||||
strncpy(part.label, label, sizeof(part.label) - 1);
|
||||
part.label[sizeof(part.label) - 1] = 0;
|
||||
part.encrypted = false;
|
||||
return part;
|
||||
}
|
||||
|
||||
esp_partition_t rawApp0Partition(uint32_t address, uint32_t size, const char* label) {
|
||||
esp_partition_t part = rawPartition(address, size, label);
|
||||
part.type = ESP_PARTITION_TYPE_APP;
|
||||
part.subtype = ESP_PARTITION_SUBTYPE_APP_OTA_0;
|
||||
return part;
|
||||
}
|
||||
|
||||
bool rangesOverlap(uint32_t first_address, uint32_t first_size,
|
||||
uint32_t second_address, uint32_t second_size) {
|
||||
const uint64_t first_end = static_cast<uint64_t>(first_address) + first_size;
|
||||
const uint64_t second_end = static_cast<uint64_t>(second_address) + second_size;
|
||||
return first_address < second_end && second_address < first_end;
|
||||
}
|
||||
|
||||
bool eraseRaw(const esp_partition_t& destination) {
|
||||
for (uint32_t offset = 0; offset < destination.size;) {
|
||||
uint32_t span = destination.size - offset;
|
||||
// A large flash erase can run long enough to trip the task watchdog on an
|
||||
// S3. Erase one sector at a time and explicitly yield between operations.
|
||||
// This is slower but keeps the board alive while its future app slot is
|
||||
// being prepared.
|
||||
if (span > migration::kSectorBytes) span = migration::kSectorBytes;
|
||||
const esp_err_t result = esp_partition_erase_range(&destination, offset, span);
|
||||
if (result != ESP_OK) {
|
||||
snprintf(status_text, sizeof(status_text), "Erase failed at 0x%lx: %s",
|
||||
(unsigned long)(destination.address + offset), errName(result));
|
||||
return false;
|
||||
}
|
||||
offset += span;
|
||||
delay(1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool copyAndVerify(const esp_partition_t& source, uint32_t destination_address,
|
||||
uint32_t bytes, const char* destination_label) {
|
||||
if (bytes == 0 || bytes > source.size || bytes % migration::kSectorBytes != 0) {
|
||||
strcpy(status_text, "Invalid migration copy geometry");
|
||||
return false;
|
||||
}
|
||||
copy_destination = rawPartition(destination_address, bytes, destination_label);
|
||||
if (!eraseRaw(copy_destination)) return false;
|
||||
|
||||
uint32_t source_crc = 0xFFFFFFFFU;
|
||||
for (uint32_t offset = 0; offset < bytes; offset += sizeof(copy_buffer)) {
|
||||
esp_err_t result = esp_partition_read(&source, offset, copy_buffer,
|
||||
sizeof(copy_buffer));
|
||||
if (result != ESP_OK) {
|
||||
snprintf(status_text, sizeof(status_text), "Read failed at 0x%lx: %s",
|
||||
(unsigned long)(source.address + offset), errName(result));
|
||||
return false;
|
||||
}
|
||||
source_crc = crc32(copy_buffer, sizeof(copy_buffer), source_crc);
|
||||
result = esp_partition_write(©_destination, offset, copy_buffer,
|
||||
sizeof(copy_buffer));
|
||||
if (result != ESP_OK) {
|
||||
snprintf(status_text, sizeof(status_text), "Write failed at 0x%lx: %s",
|
||||
(unsigned long)(copy_destination.address + offset), errName(result));
|
||||
return false;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
|
||||
uint32_t destination_crc = 0xFFFFFFFFU;
|
||||
for (uint32_t offset = 0; offset < bytes; offset += sizeof(copy_buffer)) {
|
||||
const esp_err_t result = esp_partition_read(©_destination, offset, copy_buffer,
|
||||
sizeof(copy_buffer));
|
||||
if (result != ESP_OK) {
|
||||
snprintf(status_text, sizeof(status_text), "Verify read failed at 0x%lx: %s",
|
||||
(unsigned long)(copy_destination.address + offset), errName(result));
|
||||
return false;
|
||||
}
|
||||
destination_crc = crc32(copy_buffer, sizeof(copy_buffer), destination_crc);
|
||||
delay(1);
|
||||
}
|
||||
source_crc = ~source_crc;
|
||||
destination_crc = ~destination_crc;
|
||||
if (source_crc != destination_crc) {
|
||||
snprintf(status_text, sizeof(status_text),
|
||||
"CRC mismatch while copying %s (%08lx != %08lx)", destination_label,
|
||||
(unsigned long)source_crc, (unsigned long)destination_crc);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The larger destination SPIFFS partition is allowed to reformat itself on
|
||||
// its first mount. A raw SPIFFS image is not reliably expandable, so stage
|
||||
// the one irreplaceable file in NVS first. NVS stays at the same address in
|
||||
// both layouts. The source file format is exactly the historical
|
||||
// IdentityStore layout: 32 public-key bytes followed by 64 private-key bytes.
|
||||
bool stageLegacyIdentity() {
|
||||
if (!SPIFFS.begin(false)) {
|
||||
strcpy(status_text, "Could not mount legacy SPIFFS to save identity");
|
||||
return false;
|
||||
}
|
||||
File identity = SPIFFS.open("/identity/_main.id", "r");
|
||||
const bool read_ok = identity && identity.size() >= kIdentityFileBytes
|
||||
&& identity.read(copy_buffer, kIdentityFileBytes) == kIdentityFileBytes;
|
||||
if (identity) identity.close();
|
||||
SPIFFS.end();
|
||||
if (!read_ok) {
|
||||
strcpy(status_text, "Refused: legacy private-key file is unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
Preferences migration_nvs;
|
||||
if (!migration_nvs.begin(kMigrationNvsNamespace, false)) {
|
||||
strcpy(status_text, "Could not open NVS identity staging");
|
||||
return false;
|
||||
}
|
||||
const bool saved = migration_nvs.putBytes(kMigrationIdentityKey, copy_buffer,
|
||||
kIdentityFileBytes) == kIdentityFileBytes
|
||||
&& migration_nvs.putBool(kMigrationIdentityPendingKey, true);
|
||||
migration_nvs.end();
|
||||
if (!saved) strcpy(status_text, "Could not save private key for migration");
|
||||
return saved;
|
||||
}
|
||||
|
||||
bool restoreStagedIdentity() {
|
||||
Preferences migration_nvs;
|
||||
if (!migration_nvs.begin(kMigrationNvsNamespace, false)) {
|
||||
strcpy(status_text, "Could not open NVS identity recovery");
|
||||
return false;
|
||||
}
|
||||
const bool pending = migration_nvs.getBool(kMigrationIdentityPendingKey, false);
|
||||
const size_t stored_bytes = pending
|
||||
? migration_nvs.getBytes(kMigrationIdentityKey, copy_buffer, sizeof(copy_buffer))
|
||||
: 0;
|
||||
migration_nvs.end();
|
||||
if (!pending) return true;
|
||||
if (stored_bytes != kIdentityFileBytes) {
|
||||
strcpy(status_text, "Refused: staged private key is incomplete");
|
||||
return false;
|
||||
}
|
||||
|
||||
// `true` intentionally formats only if the raw legacy SPIFFS image cannot
|
||||
// mount at the expanded size. All settings except the identity are allowed
|
||||
// to be recreated; the staged identity is immediately written back below.
|
||||
if (!SPIFFS.begin(true)) {
|
||||
strcpy(status_text, "Could not initialize expanded SPIFFS");
|
||||
return false;
|
||||
}
|
||||
if (!SPIFFS.exists("/identity") && !SPIFFS.mkdir("/identity")) {
|
||||
SPIFFS.end();
|
||||
strcpy(status_text, "Could not create identity folder");
|
||||
return false;
|
||||
}
|
||||
File identity = SPIFFS.open("/identity/_main.id", "w");
|
||||
const bool wrote = identity && identity.write(copy_buffer, kIdentityFileBytes)
|
||||
== kIdentityFileBytes;
|
||||
if (identity) {
|
||||
identity.flush();
|
||||
identity.close();
|
||||
}
|
||||
File verify = SPIFFS.open("/identity/_main.id", "r");
|
||||
const bool verified = wrote && verify && verify.size() >= kIdentityFileBytes
|
||||
&& verify.read(partition_table_verified, kIdentityFileBytes) == kIdentityFileBytes
|
||||
&& memcmp(copy_buffer, partition_table_verified, kIdentityFileBytes) == 0;
|
||||
if (verify) verify.close();
|
||||
SPIFFS.end();
|
||||
if (!verified) {
|
||||
strcpy(status_text, "Private-key restore verification failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!migration_nvs.begin(kMigrationNvsNamespace, false)) {
|
||||
strcpy(status_text, "Could not finalize NVS identity recovery");
|
||||
return false;
|
||||
}
|
||||
migration_nvs.remove(kMigrationIdentityKey);
|
||||
const bool cleared = migration_nvs.remove(kMigrationIdentityPendingKey);
|
||||
migration_nvs.end();
|
||||
if (!cleared) {
|
||||
strcpy(status_text, "Private key restored; NVS cleanup needs retry");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool publishExpandedPartitionTable(const migration::TargetPlan& plan) {
|
||||
// Preserve ESP-IDF's normal OS flash hooks. In particular, their start/end
|
||||
// hooks suspend the other core and safely disable/re-enable caches. Do not
|
||||
// call esp_flash_app_disable_protect(): that internal coredump-only helper
|
||||
// removes those hooks entirely and is unsafe from a running application.
|
||||
esp_flash_t* const chip = esp_flash_default_chip;
|
||||
const esp_flash_os_functions_t* const original_hooks = chip ? chip->os_func : nullptr;
|
||||
if (!chip || !original_hooks || !original_hooks->region_protected) {
|
||||
strcpy(status_text, "ESP-IDF flash protection hooks are unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
original_region_protected = original_hooks->region_protected;
|
||||
original_flash_hook_data = chip->os_func_data;
|
||||
partition_table_flash_hooks = *original_hooks;
|
||||
partition_table_flash_hooks.region_protected = partitionTableRegionProtected;
|
||||
chip->os_func = &partition_table_flash_hooks;
|
||||
|
||||
bool ok = false;
|
||||
// esp_partition_write disables the flash cache. The generated prefix is
|
||||
// otherwise stored in DROM, so copy it to explicitly internal DRAM first.
|
||||
memcpy(partition_table_bytes, plan.partition_table_prefix,
|
||||
plan.partition_table_prefix_bytes);
|
||||
// Do not use esp_partition_write for the table itself. Once the old sector
|
||||
// is erased, a partition-manager path must not be allowed to consult that
|
||||
// erased metadata. esp_flash_* operates on the main flash chip directly.
|
||||
esp_err_t result = esp_flash_erase_region(chip,
|
||||
migration::kPartitionTableAddress,
|
||||
migration::kPartitionTableBytes);
|
||||
if (result == ESP_OK) {
|
||||
result = esp_flash_write(chip, partition_table_bytes,
|
||||
migration::kPartitionTableAddress,
|
||||
sizeof(partition_table_bytes));
|
||||
if (result == ESP_OK) {
|
||||
result = esp_flash_read(chip, partition_table_verified,
|
||||
migration::kPartitionTableAddress,
|
||||
sizeof(partition_table_verified));
|
||||
ok = result == ESP_OK && memcmp(partition_table_verified,
|
||||
plan.partition_table_prefix, plan.partition_table_prefix_bytes) == 0;
|
||||
if (!ok) strcpy(status_text, "Partition table verification failed");
|
||||
} else {
|
||||
snprintf(status_text, sizeof(status_text), "Partition table write failed: %s",
|
||||
errName(result));
|
||||
}
|
||||
} else {
|
||||
snprintf(status_text, sizeof(status_text), "Partition table erase failed: %s",
|
||||
errName(result));
|
||||
}
|
||||
chip->os_func = original_hooks;
|
||||
original_region_protected = nullptr;
|
||||
original_flash_hook_data = nullptr;
|
||||
return ok;
|
||||
}
|
||||
|
||||
void runMigration() {
|
||||
migration_started = true;
|
||||
PartitionRefs refs;
|
||||
migration::PartitionGeometry geometry = {};
|
||||
const uint32_t flash_bytes = ESP.getFlashChipSize();
|
||||
const migration::TargetPlan* const plan = migration::targetForFlash(flash_bytes);
|
||||
if (!plan || !findPartitions(refs, geometry)
|
||||
|| !migration::canMigrateGeneric(flash_bytes, geometry)) {
|
||||
snprintf(status_text, sizeof(status_text),
|
||||
"Refused: unsupported layout or flash size (%lu bytes)",
|
||||
(unsigned long)flash_bytes);
|
||||
Serial.println(status_text);
|
||||
return;
|
||||
}
|
||||
|
||||
const esp_partition_t* running = esp_ota_get_running_partition();
|
||||
// Partition handles are opaque; their addresses, rather than their pointer
|
||||
// identities, determine the slot. This keeps the A/B decision correct if
|
||||
// ESP-IDF returns a distinct descriptor for the currently-running image.
|
||||
if (!running || (running->address != refs.app0->address &&
|
||||
running->address != refs.app1->address)) {
|
||||
strcpy(status_text, "Refused: migration image is not in a legacy OTA slot");
|
||||
Serial.println(status_text);
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("Migration: staging private key; do not interrupt power");
|
||||
if (!stageLegacyIdentity()) {
|
||||
Serial.println(status_text);
|
||||
return;
|
||||
}
|
||||
Serial.println("Migration: private key safely staged in NVS");
|
||||
|
||||
// Always make target app0 contain this migration image. If it was uploaded
|
||||
// into a different legacy slot, this preserves a Wi-Fi endpoint after the
|
||||
// table change. Never erase a running source range while copying it.
|
||||
if (running->address != plan->layout.app0_address) {
|
||||
if (running->size > plan->layout.app0_size) {
|
||||
strcpy(status_text, "Refused: migration slot is larger than future app0");
|
||||
Serial.println(status_text);
|
||||
return;
|
||||
}
|
||||
if (rangesOverlap(running->address, running->size,
|
||||
plan->layout.app0_address, running->size)) {
|
||||
strcpy(status_text, "Refused: target app0 overlaps the running image");
|
||||
Serial.println(status_text);
|
||||
return;
|
||||
}
|
||||
Serial.println("Migration: placing Wi-Fi bridge image in app0");
|
||||
if (!copyAndVerify(*running, plan->layout.app0_address,
|
||||
running->size, "future-app0")) {
|
||||
Serial.println(status_text);
|
||||
return;
|
||||
}
|
||||
Serial.println("Migration: bridge image copied to app0");
|
||||
}
|
||||
|
||||
// The OTA-select data records a slot identity. Select a descriptor for the
|
||||
// target app0 before replacing the table so the next boot runs this bridge
|
||||
// at the target table's app0 address, never the blank new app1.
|
||||
const esp_partition_t target_app0 = rawApp0Partition(plan->layout.app0_address,
|
||||
plan->layout.app0_size,
|
||||
"app0");
|
||||
const esp_err_t select_result = esp_ota_set_boot_partition(&target_app0);
|
||||
if (select_result != ESP_OK) {
|
||||
snprintf(status_text, sizeof(status_text), "Could not select app0: %s",
|
||||
errName(select_result));
|
||||
Serial.println(status_text);
|
||||
return;
|
||||
}
|
||||
Serial.println("Migration: app0 selected for restart");
|
||||
|
||||
Serial.println("Migration: publishing expanded partition table");
|
||||
// This bridge is built without native USB CDC, so a connected USB host cannot
|
||||
// post a flash-backed event while the partition-sector operation disables the
|
||||
// flash cache. Stop UART0 as well: the S3's serial event path is otherwise
|
||||
// still able to interrupt the raw flash operation. The bridge itself is
|
||||
// deliberately Wi-Fi-only; the final normal repeater build restores its
|
||||
// standard USB behavior.
|
||||
Serial.flush();
|
||||
Serial.end();
|
||||
const bool table_published = publishExpandedPartitionTable(*plan);
|
||||
Serial.begin(115200);
|
||||
delay(50);
|
||||
if (!table_published) {
|
||||
Serial.println(status_text);
|
||||
return;
|
||||
}
|
||||
|
||||
migration_complete = true;
|
||||
if (kMigrationRestartDelayMs == 0) {
|
||||
strcpy(status_text, "Partition table verified; waiting for test reboot");
|
||||
} else {
|
||||
strcpy(status_text, "Migration complete; restarting Wi-Fi uploader");
|
||||
reboot_at = millis() + kMigrationRestartDelayMs;
|
||||
}
|
||||
Serial.println(status_text);
|
||||
}
|
||||
|
||||
void sendHome(AsyncWebServerRequest* request) {
|
||||
const char* mode = status_text;
|
||||
String page;
|
||||
page.reserve(1000);
|
||||
page += "<!doctype html><meta name=viewport content='width=device-width,initial-scale=1'>";
|
||||
page += "<h2>MeshCore Wi-Fi partition migration</h2><p>";
|
||||
page += mode;
|
||||
page += "</p>";
|
||||
if (strstr(status_text, "ready") != nullptr) {
|
||||
page += "<p>The expanded partition layout is active and the private device "
|
||||
"identity was staged and restored before this uploader was exposed.</p>"
|
||||
"<p><a href='/update'>Upload the full application image for this board.</a>.</p>";
|
||||
} else if (migration_complete) {
|
||||
page += "<p>Partition-table bytes were read back successfully. The test harness "
|
||||
"is waiting for an explicit reboot.</p><p><a href='/reboot'>Restart now</a></p>";
|
||||
} else if (!migration_started) {
|
||||
page += "<p>Copying private data and replacing the partition table starts shortly. "
|
||||
"Keep USB power connected; this page will disappear while the board restarts.</p>";
|
||||
} else {
|
||||
page += "<p>Keep power connected. Refresh after two minutes if the board did not restart.</p>";
|
||||
}
|
||||
request->send(200, "text/html", page);
|
||||
}
|
||||
|
||||
void startServer() {
|
||||
// A legacy ESP-NOW repeater can leave the AP protocol mask in proprietary
|
||||
// LR mode. SoftAP then reports success but ordinary phones and laptops
|
||||
// cannot discover it. Reuse the normal WebConfig recipe: AP+STA mode,
|
||||
// an explicit interoperable protocol mask, and the project AP channel.
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
WiFi.setAutoReconnect(false);
|
||||
WiFi.disconnect(false, true);
|
||||
delay(100);
|
||||
WiFi.setSleep(false);
|
||||
const IPAddress address(192, 168, 4, 1);
|
||||
const IPAddress netmask(255, 255, 255, 0);
|
||||
if (!WiFi.softAPConfig(address, address, netmask)
|
||||
|| !WiFi.softAP(kApSsid, kApPassword, mesh::wifi::accessPointChannel())
|
||||
|| mesh::wifi::applyAccessPointProtocolMask() != ESP_OK
|
||||
|| esp_wifi_set_protocol(WIFI_IF_STA, mesh::wifi::kProtocolMask) != ESP_OK
|
||||
|| esp_wifi_set_max_tx_power(78) != ESP_OK) {
|
||||
strcpy(status_text, "Wi-Fi AP failed; restart the board and retry");
|
||||
return;
|
||||
}
|
||||
wifi_config_t ap_config = {};
|
||||
int8_t max_tx_power = 0;
|
||||
const esp_err_t config_result = esp_wifi_get_config(WIFI_IF_AP, &ap_config);
|
||||
const esp_err_t power_result = esp_wifi_get_max_tx_power(&max_tx_power);
|
||||
Serial.printf("Wi-Fi AP active: %s at %s (channel %u, hidden %u, power %.2f dBm, config %d, power %d)\n",
|
||||
WiFi.softAPSSID().c_str(), WiFi.softAPIP().toString().c_str(),
|
||||
(unsigned)ap_config.ap.channel, (unsigned)ap_config.ap.ssid_hidden,
|
||||
max_tx_power / 4.0, (int)config_result, (int)power_result);
|
||||
server.on("/", HTTP_GET, sendHome);
|
||||
server.on("/reboot", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
if (!migration_complete) {
|
||||
request->send(409, "text/plain", "Migration has not completed");
|
||||
return;
|
||||
}
|
||||
request->send(200, "text/plain", "Restarting migration bridge");
|
||||
reboot_requested = true;
|
||||
});
|
||||
AsyncElegantOTA.begin(&server);
|
||||
server.begin();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(250);
|
||||
|
||||
PartitionRefs refs;
|
||||
migration::PartitionGeometry geometry = {};
|
||||
const uint32_t flash_bytes = ESP.getFlashChipSize();
|
||||
if (findPartitions(refs, geometry)
|
||||
&& migration::isTargetLayout(flash_bytes, geometry)) {
|
||||
if (restoreStagedIdentity()) {
|
||||
strcpy(status_text, "Expanded layout ready");
|
||||
}
|
||||
} else if (findPartitions(refs, geometry)
|
||||
&& migration::canMigrateGeneric(flash_bytes, geometry)) {
|
||||
strcpy(status_text, "Legacy layout verified; migration begins shortly");
|
||||
migration_at = millis() + kMigrationDelayMs;
|
||||
} else {
|
||||
strcpy(status_text, "Refused: unsupported legacy source layout");
|
||||
}
|
||||
|
||||
startServer();
|
||||
Serial.printf("%s. Join %s (password: %s), then open http://192.168.4.1/\n",
|
||||
status_text, kApSsid, kApPassword);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (!migration_started && migration_at != 0
|
||||
&& static_cast<int32_t>(millis() - migration_at) >= 0) {
|
||||
runMigration();
|
||||
}
|
||||
if ((reboot_at != 0 && static_cast<int32_t>(millis() - reboot_at) >= 0)
|
||||
|| reboot_requested) {
|
||||
delay(100);
|
||||
ESP.restart();
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
@@ -154,8 +154,33 @@ platform_packages =
|
||||
extra_scripts = create-uf2.py
|
||||
pre:scripts/nrf52_usb_power_fix.py
|
||||
pre:scripts/nrf52_internal_bootloader_link.py
|
||||
; Nordic's builder otherwise overwrites the LTO-aware archive tools.
|
||||
pre:scripts/nrf52_lto_toolchain.py
|
||||
post:scripts/check_firmware_ram.py
|
||||
; Nordic's framework defaults to -Ofast. Prefer the size-oriented profile for
|
||||
; nRF52 images, where a smaller binary preserves OTA and filesystem headroom.
|
||||
build_unflags =
|
||||
-Ofast
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-Oz
|
||||
-flto
|
||||
; A single LTO partition avoids GCC 14 emitting out-of-range Thumb literals
|
||||
; for cross-partition clones on larger nRF52 role images.
|
||||
-flto-partition=one
|
||||
-fno-inline-small-functions
|
||||
-fipa-pta
|
||||
-fmerge-all-constants
|
||||
-fno-ipa-cp-clone
|
||||
-fno-partial-inlining
|
||||
-fno-jump-tables
|
||||
-fno-tree-switch-conversion
|
||||
-fno-semantic-interposition
|
||||
-fno-unwind-tables
|
||||
-fno-asynchronous-unwind-tables
|
||||
-fno-schedule-insns2
|
||||
-Wl,--sort-section=alignment
|
||||
; PendSV reaches this FreeRTOS function from assembly, outside the LTO graph.
|
||||
-Wl,--undefined=vTaskSwitchContext
|
||||
-D NRF52_PLATFORM
|
||||
-D LFS_NO_ASSERT=1
|
||||
-D EXTRAFS=1
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
Import("env")
|
||||
|
||||
# This script is loaded as a ``pre:`` extra script. The Nordic platform's
|
||||
# builder selects binutils ar/ranlib later while it creates the library build
|
||||
# actions. Those tools do not generate the plugin index required by GNU LTO,
|
||||
# which leaves the final link with hundreds of false "undefined reference"
|
||||
# errors. Intercept that later replacement so every nRF52 archive is built
|
||||
# with GCC's plugin-aware wrappers.
|
||||
_replace = env.Replace
|
||||
|
||||
|
||||
def _replace_with_lto_archivers(*args, **kwargs):
|
||||
if kwargs.get("AR") == "arm-none-eabi-ar":
|
||||
kwargs["AR"] = "arm-none-eabi-gcc-ar"
|
||||
if kwargs.get("RANLIB") == "arm-none-eabi-ranlib":
|
||||
kwargs["RANLIB"] = "arm-none-eabi-gcc-ranlib"
|
||||
return _replace(*args, **kwargs)
|
||||
|
||||
|
||||
env.Replace = _replace_with_lto_archivers
|
||||
|
||||
# GCC runs the final whole-program passes while linking. PlatformIO supplies
|
||||
# the project flags to compilation only, so forward the size policy explicitly
|
||||
# to that final LTO invocation as well. One partition also avoids GCC 14
|
||||
# generating out-of-range Thumb literal loads in large nRF52 images.
|
||||
env.Append(LINKFLAGS=[
|
||||
"-Oz",
|
||||
"-flto",
|
||||
"-flto-partition=one",
|
||||
"-fno-inline-small-functions",
|
||||
"-fipa-pta",
|
||||
"-fmerge-all-constants",
|
||||
"-fno-ipa-cp-clone",
|
||||
"-fno-partial-inlining",
|
||||
"-fno-jump-tables",
|
||||
"-fno-tree-switch-conversion",
|
||||
"-fno-semantic-interposition",
|
||||
"-fno-unwind-tables",
|
||||
"-fno-asynchronous-unwind-tables",
|
||||
"-fno-schedule-insns2",
|
||||
])
|
||||
@@ -0,0 +1,244 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// The bridge supports standard Arduino targets whose NVS and OTA metadata
|
||||
// remain at their normal addresses. It selects a verified target table from
|
||||
// the actual flash capacity at runtime; source application/SPIFFS sizing is
|
||||
// intentionally not hard-coded.
|
||||
namespace mesh {
|
||||
namespace esp32_partition_migration {
|
||||
|
||||
constexpr uint32_t kRequiredFlashBytes = 16U * 1024U * 1024U;
|
||||
constexpr uint32_t kXiaoRequiredFlashBytes = 8U * 1024U * 1024U;
|
||||
constexpr uint32_t kPartitionTableAddress = 0x8000;
|
||||
constexpr uint32_t kPartitionTableBytes = 0x1000;
|
||||
constexpr uint32_t kSectorBytes = 0x1000;
|
||||
|
||||
struct PartitionGeometry {
|
||||
uint32_t nvs_address;
|
||||
uint32_t nvs_size;
|
||||
uint32_t otadata_address;
|
||||
uint32_t otadata_size;
|
||||
uint32_t app0_address;
|
||||
uint32_t app0_size;
|
||||
uint32_t app1_address;
|
||||
uint32_t app1_size;
|
||||
uint32_t spiffs_address;
|
||||
uint32_t spiffs_size;
|
||||
};
|
||||
|
||||
constexpr PartitionGeometry kLegacyLayout = {
|
||||
0x9000, 0x5000,
|
||||
0xE000, 0x2000,
|
||||
0x10000, 0x140000,
|
||||
0x150000, 0x140000,
|
||||
0x290000, 0x160000,
|
||||
};
|
||||
|
||||
// This is Arduino-ESP32's default_16MB.csv. Its NVS and OTA-data locations
|
||||
// deliberately remain the same as the legacy table; SPIFFS moves after both
|
||||
// 6.25 MiB OTA slots. The bridge stages the identity separately because a
|
||||
// raw SPIFFS image cannot safely be expanded in place.
|
||||
constexpr PartitionGeometry kExpandedLayout = {
|
||||
0x9000, 0x5000,
|
||||
0xE000, 0x2000,
|
||||
0x10000, 0x640000,
|
||||
0x650000, 0x640000,
|
||||
0xC90000, 0x360000,
|
||||
};
|
||||
|
||||
// Arduino-ESP32's default_8MB.csv, used by the Seeed XIAO ESP32-S3 and other
|
||||
// 8 MiB boards. Its metadata locations intentionally match the 16 MiB map.
|
||||
constexpr PartitionGeometry kExpanded8MBLayout = {
|
||||
0x9000, 0x5000,
|
||||
0xE000, 0x2000,
|
||||
0x10000, 0x330000,
|
||||
0x340000, 0x330000,
|
||||
0x670000, 0x180000,
|
||||
};
|
||||
|
||||
constexpr bool sameGeometry(const PartitionGeometry& left,
|
||||
const PartitionGeometry& right) {
|
||||
return left.nvs_address == right.nvs_address
|
||||
&& left.nvs_size == right.nvs_size
|
||||
&& left.otadata_address == right.otadata_address
|
||||
&& left.otadata_size == right.otadata_size
|
||||
&& left.app0_address == right.app0_address
|
||||
&& left.app0_size == right.app0_size
|
||||
&& left.app1_address == right.app1_address
|
||||
&& left.app1_size == right.app1_size
|
||||
&& left.spiffs_address == right.spiffs_address
|
||||
&& left.spiffs_size == right.spiffs_size;
|
||||
}
|
||||
|
||||
constexpr bool isLegacyLayout(const PartitionGeometry& geometry) {
|
||||
return sameGeometry(geometry, kLegacyLayout);
|
||||
}
|
||||
|
||||
constexpr bool isExpandedLayout(const PartitionGeometry& geometry) {
|
||||
return sameGeometry(geometry, kExpandedLayout);
|
||||
}
|
||||
|
||||
constexpr bool canMigrate(uint32_t flash_bytes,
|
||||
const PartitionGeometry& geometry) {
|
||||
return flash_bytes >= kRequiredFlashBytes && isLegacyLayout(geometry);
|
||||
}
|
||||
|
||||
// Binary prefix generated from Arduino-ESP32's default_16MB.csv by
|
||||
// gen_esp32part.py. The caller erases the complete 4 KiB partition-table
|
||||
// sector first, then writes this prefix; the already-erased suffix is the
|
||||
// required 0xFF padding. Keeping the generated MD5 record makes the
|
||||
// bootloader reject an accidental partial or altered entry list.
|
||||
constexpr uint8_t kExpandedPartitionTablePrefix[] = {
|
||||
0xAA, 0x50, 0x01, 0x02, 0x00, 0x90, 0x00, 0x00,
|
||||
0x00, 0x50, 0x00, 0x00, 0x6E, 0x76, 0x73, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00,
|
||||
0x00, 0x20, 0x00, 0x00, 0x6F, 0x74, 0x61, 0x64,
|
||||
0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x64, 0x00, 0x61, 0x70, 0x70, 0x30,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x00, 0x11, 0x00, 0x00, 0x65, 0x00,
|
||||
0x00, 0x00, 0x64, 0x00, 0x61, 0x70, 0x70, 0x31,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x01, 0x82, 0x00, 0x00, 0xC9, 0x00,
|
||||
0x00, 0x00, 0x36, 0x00, 0x73, 0x70, 0x69, 0x66,
|
||||
0x66, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x01, 0x03, 0x00, 0x00, 0xFF, 0x00,
|
||||
0x00, 0x00, 0x01, 0x00, 0x63, 0x6F, 0x72, 0x65,
|
||||
0x64, 0x75, 0x6D, 0x70, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xEB, 0xEB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xB3, 0x2D, 0xCD, 0xB8, 0x3B, 0x16, 0x74, 0xAF,
|
||||
0x27, 0x76, 0x8C, 0x92, 0xB1, 0xF8, 0x8A, 0x62,
|
||||
};
|
||||
|
||||
constexpr size_t kExpandedPartitionTablePrefixBytes =
|
||||
sizeof(kExpandedPartitionTablePrefix);
|
||||
|
||||
// Binary prefix generated from Arduino-ESP32's default_8MB.csv.
|
||||
constexpr uint8_t kExpanded8MBPartitionTablePrefix[] = {
|
||||
0xAA, 0x50, 0x01, 0x02, 0x00, 0x90, 0x00, 0x00,
|
||||
0x00, 0x50, 0x00, 0x00, 0x6E, 0x76, 0x73, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x01, 0x00, 0x00, 0xE0, 0x00, 0x00,
|
||||
0x00, 0x20, 0x00, 0x00, 0x6F, 0x74, 0x61, 0x64,
|
||||
0x61, 0x74, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x33, 0x00, 0x61, 0x70, 0x70, 0x30,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x00, 0x11, 0x00, 0x00, 0x34, 0x00,
|
||||
0x00, 0x00, 0x33, 0x00, 0x61, 0x70, 0x70, 0x31,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x01, 0x82, 0x00, 0x00, 0x67, 0x00,
|
||||
0x00, 0x00, 0x18, 0x00, 0x73, 0x70, 0x69, 0x66,
|
||||
0x66, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xAA, 0x50, 0x01, 0x03, 0x00, 0x00, 0x7F, 0x00,
|
||||
0x00, 0x00, 0x01, 0x00, 0x63, 0x6F, 0x72, 0x65,
|
||||
0x64, 0x75, 0x6D, 0x70, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xEB, 0xEB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0x46, 0x7E, 0xB8, 0x96, 0xE2, 0x9D, 0x1A, 0xA9,
|
||||
0x38, 0xC5, 0x57, 0xF4, 0xCD, 0xFC, 0x4C, 0x5B,
|
||||
};
|
||||
|
||||
constexpr size_t kExpanded8MBPartitionTablePrefixBytes =
|
||||
sizeof(kExpanded8MBPartitionTablePrefix);
|
||||
|
||||
struct TargetPlan {
|
||||
uint32_t flash_bytes;
|
||||
const char* name;
|
||||
PartitionGeometry layout;
|
||||
const uint8_t* partition_table_prefix;
|
||||
size_t partition_table_prefix_bytes;
|
||||
};
|
||||
|
||||
constexpr TargetPlan kTarget8MB = {
|
||||
kXiaoRequiredFlashBytes, "Arduino default_8MB",
|
||||
kExpanded8MBLayout, kExpanded8MBPartitionTablePrefix,
|
||||
kExpanded8MBPartitionTablePrefixBytes,
|
||||
};
|
||||
|
||||
constexpr TargetPlan kTarget16MB = {
|
||||
kRequiredFlashBytes, "Arduino default_16MB",
|
||||
kExpandedLayout, kExpandedPartitionTablePrefix,
|
||||
kExpandedPartitionTablePrefixBytes,
|
||||
};
|
||||
|
||||
constexpr const TargetPlan* targetForFlash(uint32_t flash_bytes) {
|
||||
return flash_bytes == kXiaoRequiredFlashBytes ? &kTarget8MB
|
||||
: flash_bytes == kRequiredFlashBytes ? &kTarget16MB
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
constexpr bool hasStableMetadata(const PartitionGeometry& geometry) {
|
||||
return geometry.nvs_address == kLegacyLayout.nvs_address
|
||||
&& geometry.nvs_size == kLegacyLayout.nvs_size
|
||||
&& geometry.otadata_address == kLegacyLayout.otadata_address
|
||||
&& geometry.otadata_size == kLegacyLayout.otadata_size;
|
||||
}
|
||||
|
||||
inline bool isTargetLayout(uint32_t flash_bytes,
|
||||
const PartitionGeometry& geometry) {
|
||||
const TargetPlan* const plan = targetForFlash(flash_bytes);
|
||||
return plan && sameGeometry(geometry, plan->layout);
|
||||
}
|
||||
|
||||
inline bool partitionFitsFlash(uint32_t flash_bytes, uint32_t address,
|
||||
uint32_t size) {
|
||||
return size != 0
|
||||
&& static_cast<uint64_t>(address) + static_cast<uint64_t>(size)
|
||||
<= static_cast<uint64_t>(flash_bytes);
|
||||
}
|
||||
|
||||
// Source layouts may vary in their two OTA-slot sizes and SPIFFS placement.
|
||||
// The updater itself ensures the bridge fits its inactive slot. We only
|
||||
// accept layouts which retain NVS/otadata at the exact addresses the target
|
||||
// keeps, have two distinct non-empty OTA slots, and expose a SPIFFS partition
|
||||
// from which the identity can be staged.
|
||||
inline bool canMigrateGeneric(uint32_t flash_bytes,
|
||||
const PartitionGeometry& geometry) {
|
||||
const TargetPlan* const plan = targetForFlash(flash_bytes);
|
||||
return plan && hasStableMetadata(geometry)
|
||||
&& partitionFitsFlash(flash_bytes, geometry.app0_address, geometry.app0_size)
|
||||
&& partitionFitsFlash(flash_bytes, geometry.app1_address, geometry.app1_size)
|
||||
&& partitionFitsFlash(flash_bytes, geometry.spiffs_address, geometry.spiffs_size)
|
||||
&& geometry.app0_address != geometry.app1_address
|
||||
&& !sameGeometry(geometry, plan->layout);
|
||||
}
|
||||
|
||||
static_assert(kExpandedLayout.app0_address + kExpandedLayout.app0_size
|
||||
== kExpandedLayout.app1_address,
|
||||
"expanded OTA slots must be adjacent");
|
||||
static_assert(kExpandedLayout.app1_address + kExpandedLayout.app1_size
|
||||
== kExpandedLayout.spiffs_address,
|
||||
"expanded SPIFFS must follow both OTA slots");
|
||||
static_assert(kExpandedLayout.spiffs_size >= kLegacyLayout.spiffs_size,
|
||||
"the full legacy SPIFFS image must fit in the target partition");
|
||||
static_assert(kExpanded8MBLayout.app0_address + kExpanded8MBLayout.app0_size
|
||||
== kExpanded8MBLayout.app1_address,
|
||||
"8 MiB expanded OTA slots must be adjacent");
|
||||
static_assert(kExpanded8MBLayout.app1_address + kExpanded8MBLayout.app1_size
|
||||
== kExpanded8MBLayout.spiffs_address,
|
||||
"8 MiB expanded SPIFFS must follow both OTA slots");
|
||||
static_assert(kExpandedPartitionTablePrefixBytes
|
||||
== kExpanded8MBPartitionTablePrefixBytes,
|
||||
"target table buffers share one fixed DRAM size");
|
||||
|
||||
} // namespace esp32_partition_migration
|
||||
} // namespace mesh
|
||||
+96
-14
@@ -319,6 +319,37 @@ require(rak_usb, "build_flags", "FORCE_GPS_ALIVE")
|
||||
# replace each with that exact board's Full target.
|
||||
init_project_context >/dev/null
|
||||
|
||||
# ESP32 and nRF52 standalone room servers receive the same lean LoRa-OTA
|
||||
# sibling as repeaters. Sensor roles stay on their ordinary recipe so this
|
||||
# cannot accidentally become a blanket no-external-sensors policy.
|
||||
while IFS='|' read -r room_env room_ota_env; do
|
||||
is_supported_build_env "$room_ota_env" \
|
||||
|| fail "$room_ota_env was not registered as a room-server LoRa OTA target"
|
||||
[ "$(get_pio_build_env "$room_ota_env")" = "$room_env" ] \
|
||||
|| fail "$room_ota_env did not retain its room-server PlatformIO recipe"
|
||||
[ "$(get_reduced_lora_ota_target "$room_env")" = "$room_ota_env" ] \
|
||||
|| fail "$room_env did not resolve its reduced room-server OTA sibling"
|
||||
(
|
||||
BUILD_PROFILE_FOR_TARGET=standard
|
||||
ESP32_FULL_BUILD=0
|
||||
is_lora_ota_build "$room_ota_env" \
|
||||
|| fail "$room_ota_env did not enable LoRa OTA"
|
||||
)
|
||||
done <<'ROOM_OTA_SPECS'
|
||||
Heltec_v3_room_server|Heltec_v3_room_server_lora_ota_no_external_sensors
|
||||
RAK_3401_room_server|RAK_3401_room_server_lora_ota_no_external_sensors
|
||||
ROOM_OTA_SPECS
|
||||
print_release_firmware_targets get-room-server-firmwares-to-build | grep -Fx \
|
||||
RAK_3401_room_server_lora_ota_no_external_sensors >/dev/null \
|
||||
|| fail "room-server release resolution omitted its portable LoRa OTA target"
|
||||
for sensor_ota_env in \
|
||||
Heltec_v3_sensor_lora_ota_no_external_sensors \
|
||||
RAK_3401_sensor_lora_ota_no_external_sensors; do
|
||||
if is_supported_build_env "$sensor_ota_env"; then
|
||||
fail "$sensor_ota_env was incorrectly generated from a sensor role"
|
||||
fi
|
||||
done
|
||||
|
||||
# KISS inherits board OTA flags, not the application OTA implementation.
|
||||
for kiss_env in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
is_kiss_modem_target "$kiss_env" || continue
|
||||
@@ -561,16 +592,20 @@ unset PLATFORMIO_BUILD_FLAGS PLATFORMIO_BUILD_UNFLAGS \
|
||||
PLATFORMIO_BUILD_SRC_FILTER
|
||||
|
||||
# An environment that includes only the tiny OTA inflater still needs the C++
|
||||
# protocol implementation when build.sh overlays LoRa OTA.
|
||||
reduced_nrf_ota_env=RAK_3401_repeater_lora_ota_no_external_sensors
|
||||
reduced_nrf_pio_env=$(get_pio_build_env "$reduced_nrf_ota_env")
|
||||
PLATFORMIO_BUILD_SRC_FILTER=""
|
||||
PLATFORMIO_EXTRA_SCRIPTS=""
|
||||
apply_nrf52_lora_ota_build_recipe "$reduced_nrf_ota_env" "$reduced_nrf_pio_env"
|
||||
if ! pio_env_option_contains "$reduced_nrf_pio_env" build_src_filter "helpers/ota/*.cpp" \
|
||||
&& [[ "$PLATFORMIO_BUILD_SRC_FILTER" != *"helpers/ota/*.cpp"* ]]; then
|
||||
fail "$reduced_nrf_ota_env omitted the OTA C++ implementation"
|
||||
fi
|
||||
# protocol implementation when build.sh overlays LoRa OTA. Room servers use
|
||||
# the same nRF52 overlay as repeaters.
|
||||
for reduced_nrf_ota_env in \
|
||||
RAK_3401_repeater_lora_ota_no_external_sensors \
|
||||
RAK_3401_room_server_lora_ota_no_external_sensors; do
|
||||
reduced_nrf_pio_env=$(get_pio_build_env "$reduced_nrf_ota_env")
|
||||
PLATFORMIO_BUILD_SRC_FILTER=""
|
||||
PLATFORMIO_EXTRA_SCRIPTS=""
|
||||
apply_nrf52_lora_ota_build_recipe "$reduced_nrf_ota_env" "$reduced_nrf_pio_env"
|
||||
if ! pio_env_option_contains "$reduced_nrf_pio_env" build_src_filter "helpers/ota/*.cpp" \
|
||||
&& [[ "$PLATFORMIO_BUILD_SRC_FILTER" != *"helpers/ota/*.cpp"* ]]; then
|
||||
fail "$reduced_nrf_ota_env omitted the OTA C++ implementation"
|
||||
fi
|
||||
done
|
||||
unset PLATFORMIO_BUILD_SRC_FILTER PLATFORMIO_EXTRA_SCRIPTS
|
||||
|
||||
# The bounded AsyncTCP task stack belongs to every ESP32 Full profile, including
|
||||
@@ -727,26 +762,40 @@ for logging_target in Station_G2_companion_radio_full RAK_4631_companion_radio_f
|
||||
verify_full_logging_contract "$logging_target" 0 off 1 yes no
|
||||
done
|
||||
|
||||
# Synthetic inventory: one ESP32 target qualified for expanded Full and one
|
||||
# nRF52 target which must attempt complete LoRa OTA in its current partition.
|
||||
# Synthetic inventory: ESP32 and nRF52 repeater/room-server targets with the
|
||||
# same lean OTA policy. The sensor deliberately has no lean sibling.
|
||||
SUPPORTED_PIO_ENVS=(
|
||||
esp_repeater
|
||||
esp_repeater_lora_ota_no_external_sensors
|
||||
esp_room_server
|
||||
esp_room_server_lora_ota_no_external_sensors
|
||||
nrf_repeater
|
||||
nrf_repeater_lora_ota_no_external_sensors
|
||||
nrf_room_server
|
||||
nrf_room_server_lora_ota_no_external_sensors
|
||||
nrf_qspi_repeater
|
||||
nrf_qspi_repeater_lora_ota_no_external_sensors
|
||||
nrf_qspi_room_server
|
||||
nrf_qspi_room_server_lora_ota_no_external_sensors
|
||||
nrf_sensor
|
||||
)
|
||||
PIO_ENV_PLATFORM_BY_NAME[esp_repeater]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[esp_repeater_lora_ota_no_external_sensors]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[esp_room_server]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[esp_room_server_lora_ota_no_external_sensors]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_repeater]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_repeater_lora_ota_no_external_sensors]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_room_server]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_room_server_lora_ota_no_external_sensors]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_qspi_repeater]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_qspi_repeater_lora_ota_no_external_sensors]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_qspi_room_server]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_qspi_room_server_lora_ota_no_external_sensors]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_sensor]=NRF52_PLATFORM
|
||||
PIO_ENV_QSPI_OTA_BY_NAME[nrf_qspi_repeater_lora_ota_no_external_sensors]=1
|
||||
PIO_ENV_QSPI_OTA_BY_NAME[nrf_qspi_room_server_lora_ota_no_external_sensors]=1
|
||||
PIO_ENV_FULL_BUILD_BY_NAME[esp_repeater]=1
|
||||
PIO_ENV_FULL_BUILD_BY_NAME[esp_room_server]=1
|
||||
|
||||
RESOLVED_BUILD_TARGETS=(esp_repeater)
|
||||
configure_effective_build_profile build-firmware >/dev/null
|
||||
@@ -756,6 +805,16 @@ configure_effective_build_profile build-firmware >/dev/null
|
||||
esp_repeater_lora_ota_no_external_sensors ] \
|
||||
|| fail "ESP32 reduced fallback was not resolved"
|
||||
|
||||
BUILD_PROFILE_OVERRIDE=auto
|
||||
BUILD_PROFILE_EXPLICIT=0
|
||||
RESOLVED_BUILD_TARGETS=(esp_room_server)
|
||||
configure_effective_build_profile build-firmware >/dev/null
|
||||
[ "$BUILD_PROFILE_EFFECTIVE" = full ] || fail "ESP32 room server auto did not prefer Full"
|
||||
[ "$AUTO_PREFER_FULL_BUILD" = 1 ] || fail "ESP32 room-server Full pass was not scheduled"
|
||||
[ "$AUTO_REDUCED_FALLBACK_TARGET" = \
|
||||
esp_room_server_lora_ota_no_external_sensors ] \
|
||||
|| fail "ESP32 room-server reduced fallback was not resolved"
|
||||
|
||||
BUILD_PROFILE_OVERRIDE=auto
|
||||
BUILD_PROFILE_EXPLICIT=0
|
||||
RESOLVED_BUILD_TARGETS=(nrf_repeater)
|
||||
@@ -768,6 +827,18 @@ configure_effective_build_profile build-firmware >/dev/null
|
||||
[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 1 ] \
|
||||
|| fail "internal-flash nRF52 reduced second artifact was not scheduled"
|
||||
|
||||
BUILD_PROFILE_OVERRIDE=auto
|
||||
BUILD_PROFILE_EXPLICIT=0
|
||||
RESOLVED_BUILD_TARGETS=(nrf_room_server)
|
||||
configure_effective_build_profile build-firmware >/dev/null
|
||||
[ "${RESOLVED_BUILD_TARGETS[0]}" = \
|
||||
nrf_room_server_lora_ota_no_external_sensors ] \
|
||||
|| fail "nRF52 room-server complete OTA pass did not select the OTA identity"
|
||||
[ "$AUTO_COMPLETE_FIRST_PASS" = 1 ] \
|
||||
|| fail "nRF52 room-server complete first pass was not scheduled"
|
||||
[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 1 ] \
|
||||
|| fail "internal-flash nRF52 room-server reduced second artifact was not scheduled"
|
||||
|
||||
BUILD_PROFILE_OVERRIDE=auto
|
||||
BUILD_PROFILE_EXPLICIT=0
|
||||
RESOLVED_BUILD_TARGETS=(nrf_qspi_repeater)
|
||||
@@ -777,6 +848,15 @@ configure_effective_build_profile build-firmware >/dev/null
|
||||
[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 0 ] \
|
||||
|| fail "external-storage nRF52 incorrectly scheduled a redundant artifact"
|
||||
|
||||
BUILD_PROFILE_OVERRIDE=auto
|
||||
BUILD_PROFILE_EXPLICIT=0
|
||||
RESOLVED_BUILD_TARGETS=(nrf_qspi_room_server)
|
||||
configure_effective_build_profile build-firmware >/dev/null
|
||||
[ "$AUTO_COMPLETE_FIRST_PASS" = 1 ] \
|
||||
|| fail "external-storage nRF52 room-server complete first pass was not scheduled"
|
||||
[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 0 ] \
|
||||
|| fail "external-storage nRF52 room-server incorrectly scheduled a redundant artifact"
|
||||
|
||||
BUILD_PROFILE_OVERRIDE=auto
|
||||
BUILD_PROFILE_EXPLICIT=0
|
||||
RESOLVED_BUILD_TARGETS=(nrf_sensor)
|
||||
@@ -784,9 +864,9 @@ configure_effective_build_profile build-firmware >/dev/null
|
||||
[ "$BUILD_PROFILE_EFFECTIVE" = auto ] \
|
||||
|| fail "nRF52 sensor did not retain its ordinary auto profile"
|
||||
[ "$AUTO_COMPLETE_FIRST_PASS" = 0 ] \
|
||||
|| fail "non-repeater nRF52 target incorrectly scheduled OTA profile passes"
|
||||
|| fail "nRF52 sensor incorrectly scheduled OTA profile passes"
|
||||
[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 0 ] \
|
||||
|| fail "non-repeater nRF52 target incorrectly scheduled two artifacts"
|
||||
|| fail "nRF52 sensor incorrectly scheduled two artifacts"
|
||||
|
||||
# Ordinary USB-loggable roles compile their historical logging profile into
|
||||
# the canonical artifact. LoRa OTA repeaters, KISS, and BLE keep their distinct
|
||||
@@ -1018,6 +1098,8 @@ verify_reduced_sensor_flags() {
|
||||
|
||||
verify_reduced_sensor_flags \
|
||||
RAK_3401_repeater_lora_ota_no_external_sensors 1
|
||||
verify_reduced_sensor_flags \
|
||||
RAK_3401_room_server_lora_ota_no_external_sensors 1
|
||||
verify_reduced_sensor_flags \
|
||||
Heltec_t114_repeater_lora_ota_no_external_sensors 0
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <helpers/ESP32PartitionMigrationPolicy.h>
|
||||
|
||||
namespace policy = mesh::esp32_partition_migration;
|
||||
|
||||
TEST(Esp32PartitionMigrationPolicy, AcceptsOnlyTheKnownLegacyV4Layout) {
|
||||
EXPECT_TRUE(policy::isLegacyLayout(policy::kLegacyLayout));
|
||||
EXPECT_TRUE(policy::canMigrate(policy::kRequiredFlashBytes,
|
||||
policy::kLegacyLayout));
|
||||
EXPECT_TRUE(policy::canMigrate(policy::kRequiredFlashBytes + 1,
|
||||
policy::kLegacyLayout));
|
||||
EXPECT_FALSE(policy::canMigrate(policy::kRequiredFlashBytes - 1,
|
||||
policy::kLegacyLayout));
|
||||
|
||||
policy::PartitionGeometry shifted = policy::kLegacyLayout;
|
||||
shifted.spiffs_address += policy::kSectorBytes;
|
||||
EXPECT_FALSE(policy::isLegacyLayout(shifted));
|
||||
EXPECT_FALSE(policy::canMigrate(policy::kRequiredFlashBytes, shifted));
|
||||
}
|
||||
|
||||
TEST(Esp32PartitionMigrationPolicy, ExpandedLayoutRetainsAndGrowsSpiffs) {
|
||||
EXPECT_TRUE(policy::isExpandedLayout(policy::kExpandedLayout));
|
||||
EXPECT_EQ(0x9000U, policy::kExpandedLayout.nvs_address);
|
||||
EXPECT_EQ(policy::kLegacyLayout.nvs_size, policy::kExpandedLayout.nvs_size);
|
||||
EXPECT_EQ(policy::kLegacyLayout.otadata_address,
|
||||
policy::kExpandedLayout.otadata_address);
|
||||
EXPECT_LT(policy::kLegacyLayout.spiffs_address,
|
||||
policy::kExpandedLayout.spiffs_address);
|
||||
EXPECT_GE(policy::kExpandedLayout.spiffs_size,
|
||||
policy::kLegacyLayout.spiffs_size);
|
||||
EXPECT_EQ(policy::kExpandedLayout.app1_address + policy::kExpandedLayout.app1_size,
|
||||
policy::kExpandedLayout.spiffs_address);
|
||||
}
|
||||
|
||||
TEST(Esp32PartitionMigrationPolicy, GenericPlansAcceptStableEightAndSixteenMiBLayouts) {
|
||||
EXPECT_EQ(&policy::kTarget8MB, policy::targetForFlash(8U * 1024U * 1024U));
|
||||
EXPECT_EQ(&policy::kTarget16MB, policy::targetForFlash(16U * 1024U * 1024U));
|
||||
EXPECT_EQ(nullptr, policy::targetForFlash(4U * 1024U * 1024U));
|
||||
EXPECT_TRUE(policy::isTargetLayout(8U * 1024U * 1024U,
|
||||
policy::kExpanded8MBLayout));
|
||||
EXPECT_TRUE(policy::canMigrateGeneric(8U * 1024U * 1024U,
|
||||
policy::kLegacyLayout));
|
||||
EXPECT_TRUE(policy::canMigrateGeneric(16U * 1024U * 1024U,
|
||||
policy::kLegacyLayout));
|
||||
|
||||
policy::PartitionGeometry moved_metadata = policy::kLegacyLayout;
|
||||
moved_metadata.nvs_address += policy::kSectorBytes;
|
||||
EXPECT_FALSE(policy::canMigrateGeneric(8U * 1024U * 1024U,
|
||||
moved_metadata));
|
||||
|
||||
policy::PartitionGeometry out_of_bounds = policy::kLegacyLayout;
|
||||
out_of_bounds.app1_address = 8U * 1024U * 1024U - policy::kSectorBytes;
|
||||
EXPECT_FALSE(policy::canMigrateGeneric(8U * 1024U * 1024U,
|
||||
out_of_bounds));
|
||||
}
|
||||
|
||||
TEST(Esp32PartitionMigrationPolicy, GeneratedTablePrefixContainsAllSixEntriesAndMd5) {
|
||||
EXPECT_EQ(0xE0U, policy::kExpandedPartitionTablePrefixBytes);
|
||||
EXPECT_EQ(0xAA, policy::kExpandedPartitionTablePrefix[0]);
|
||||
EXPECT_EQ(0x50, policy::kExpandedPartitionTablePrefix[1]);
|
||||
EXPECT_EQ(0xAA, policy::kExpandedPartitionTablePrefix[0xA0]);
|
||||
EXPECT_EQ(0x50, policy::kExpandedPartitionTablePrefix[0xA1]);
|
||||
EXPECT_EQ(0xEB, policy::kExpandedPartitionTablePrefix[0xC0]);
|
||||
EXPECT_EQ(0xEB, policy::kExpandedPartitionTablePrefix[0xC1]);
|
||||
EXPECT_EQ(policy::kExpandedPartitionTablePrefixBytes,
|
||||
policy::kExpanded8MBPartitionTablePrefixBytes);
|
||||
EXPECT_EQ(0x33, policy::kExpanded8MBPartitionTablePrefix[0x4A]);
|
||||
EXPECT_EQ(0x34, policy::kExpanded8MBPartitionTablePrefix[0x66]);
|
||||
EXPECT_EQ(0x67, policy::kExpanded8MBPartitionTablePrefix[0x86]);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Contract checks for the shared ESP32 Wi-Fi partition migrator."""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HEADER = ROOT / "src/helpers/ESP32PartitionMigrationPolicy.h"
|
||||
SOURCE = ROOT / "examples/esp32_partition_migrator/main.cpp"
|
||||
PROFILE = ROOT / "variants/heltec_v4/platformio.ini"
|
||||
MIGRATOR_BOARD = ROOT / "boards/heltec_v4_migrator.json"
|
||||
XIAO_PROFILE = ROOT / "variants/xiao_s3_wio/platformio.ini"
|
||||
XIAO_MIGRATOR_BOARD = ROOT / "boards/seeed_xiao_esp32s3_migrator.json"
|
||||
PARTITIONS = Path.home() / ".platformio/packages/framework-arduinoespressif32/tools/partitions/default_16MB.csv"
|
||||
PARTITIONS_8MB = Path.home() / ".platformio/packages/framework-arduinoespressif32/tools/partitions/default_8MB.csv"
|
||||
GENERATOR = Path.home() / ".platformio/packages/framework-arduinoespressif32/tools/gen_esp32part.py"
|
||||
|
||||
|
||||
class HeltecV4PartitionMigratorTest(unittest.TestCase):
|
||||
def test_profile_is_a_small_legacy_slot_image_with_its_own_wifi_uploader(self):
|
||||
profile = PROFILE.read_text(encoding="utf-8")
|
||||
start = profile.index("[env:heltec_v4_partition_migrator]")
|
||||
section = profile[start:]
|
||||
self.assertIn("board = heltec_v4_migrator", section)
|
||||
self.assertIn("board_build.partitions = default.csv", section)
|
||||
self.assertIn("+<../examples/esp32_partition_migrator>", section)
|
||||
self.assertIn("file://arch/esp32/AsyncElegantOTA", section)
|
||||
|
||||
xiao_profile = XIAO_PROFILE.read_text(encoding="utf-8")
|
||||
self.assertIn("[env:xiao_s3_partition_migrator]", xiao_profile)
|
||||
self.assertIn("board = seeed_xiao_esp32s3_migrator", xiao_profile)
|
||||
self.assertIn("[env:xiao_s3_partition_legacy_seed]", xiao_profile)
|
||||
self.assertIn("board_build.partitions = default.csv", xiao_profile)
|
||||
xiao_board = XIAO_MIGRATOR_BOARD.read_text(encoding="utf-8")
|
||||
self.assertIn('"flash_size": "8MB"', xiao_board)
|
||||
|
||||
def test_migration_stages_identity_and_self_copies_before_table_switch(self):
|
||||
source = SOURCE.read_text(encoding="utf-8")
|
||||
self.assertIn("bool stageLegacyIdentity()", source)
|
||||
self.assertIn("bool restoreStagedIdentity()", source)
|
||||
self.assertIn("/identity/_main.id", source)
|
||||
self.assertIn('Preferences migration_nvs', source)
|
||||
self.assertIn('kMigrationIdentityPendingKey[] = "id-pending"', source)
|
||||
self.assertIn("migration::canMigrateGeneric", source)
|
||||
self.assertIn("migration::isTargetLayout", source)
|
||||
self.assertIn("migration::targetForFlash", source)
|
||||
self.assertIn("copyAndVerify(*running", source)
|
||||
self.assertIn("plan->layout.app0_address", source)
|
||||
self.assertIn("running->size > plan->layout.app0_size", source)
|
||||
migration_body = source[source.index("void runMigration()") :]
|
||||
self.assertLess(migration_body.index("stageLegacyIdentity()"),
|
||||
migration_body.index("copyAndVerify(*running"))
|
||||
self.assertLess(migration_body.index("esp_ota_set_boot_partition(&target_app0)"),
|
||||
migration_body.index("publishExpandedPartitionTable(*plan)"))
|
||||
setup_body = source[source.index("void setup()") :]
|
||||
self.assertIn("restoreStagedIdentity()", setup_body)
|
||||
self.assertIn("AsyncElegantOTA.begin(&server)", source)
|
||||
self.assertIn("MeshCore-Migrate", source)
|
||||
self.assertIn("DRAM_ATTR esp_partition_t copy_destination", source)
|
||||
self.assertIn("partition_table_flash_hooks.region_protected = partitionTableRegionProtected", source)
|
||||
self.assertIn("chip->os_func = &partition_table_flash_hooks", source)
|
||||
self.assertIn("esp_flash_erase_region(chip", source)
|
||||
self.assertIn("esp_flash_write(chip", source)
|
||||
self.assertNotIn("#include <esp_flash_internal.h>", source)
|
||||
|
||||
board = MIGRATOR_BOARD.read_text(encoding="utf-8")
|
||||
self.assertIn('"-DARDUINO_USB_CDC_ON_BOOT=0"', board)
|
||||
|
||||
def test_embedded_partition_prefixes_match_arduino_default_targets(self):
|
||||
self.assertTrue(PARTITIONS.is_file(), "Arduino partition CSV is required")
|
||||
self.assertTrue(PARTITIONS_8MB.is_file(), "Arduino 8 MiB partition CSV is required")
|
||||
self.assertTrue(GENERATOR.is_file(), "Arduino partition generator is required")
|
||||
text = HEADER.read_text(encoding="utf-8")
|
||||
block = text[text.index("kExpandedPartitionTablePrefix[]"):
|
||||
text.index("};", text.index("kExpandedPartitionTablePrefix[]"))]
|
||||
embedded = bytes(int(value, 16) for value in re.findall(r"0x([0-9A-F]{2})", block))
|
||||
self.assertEqual(0xE0, len(embedded))
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-v4-partition-") as directory:
|
||||
generated = Path(directory) / "default16.bin"
|
||||
subprocess.run([sys.executable, str(GENERATOR), str(PARTITIONS), str(generated)],
|
||||
check=True, capture_output=True, text=True)
|
||||
self.assertEqual(embedded, generated.read_bytes()[:len(embedded)])
|
||||
generated_8mb = Path(directory) / "default8.bin"
|
||||
subprocess.run([sys.executable, str(GENERATOR), str(PARTITIONS_8MB), str(generated_8mb)],
|
||||
check=True, capture_output=True, text=True)
|
||||
start_8mb = text.index("kExpanded8MBPartitionTablePrefix[]")
|
||||
block_8mb = text[start_8mb:text.index("};", start_8mb)]
|
||||
embedded_8mb = bytes(int(value, 16)
|
||||
for value in re.findall(r"0x([0-9A-F]{2})", block_8mb))
|
||||
self.assertEqual(embedded_8mb, generated_8mb.read_bytes()[:len(embedded_8mb)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -29,7 +29,7 @@ build_flags = ${rak4631_hw.build_flags}
|
||||
-D P_LORA_PA_POWER=30 ;VFEM_Ctrl -LDO power enable
|
||||
-D P_LORA_KCT8103L_PA_CSD=12
|
||||
-D P_LORA_KCT8103L_PA_CTX=41
|
||||
-D LORA_TX_POWER=9 ; 9dBm + ~13dB KCT8103L gain = ~22dBm output
|
||||
-D LORA_TX_POWER=22 ; Default to maximum SX1262 drive (~28dBm board output)
|
||||
-D MAX_LORA_TX_POWER=22 ; Max SX1262 output -> ~28dBm at antenna
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
|
||||
@@ -12,7 +12,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
-D LORA_TX_POWER=12
|
||||
-D LORA_TX_POWER=22 ; Default to maximum SX1262 drive
|
||||
-D MAX_LORA_TX_POWER=22 ; Max SX1262 output -> ~29dBm at antenna
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
|
||||
@@ -27,7 +27,7 @@ build_flags =
|
||||
-D PIN_USER_BTN=0
|
||||
-D PIN_VEXT_EN=36
|
||||
-D PIN_VEXT_EN_ACTIVE=HIGH
|
||||
-D LORA_TX_POWER=10 ;If it is configured as 10 here, the final output will be 22 dbm.
|
||||
-D LORA_TX_POWER=22 ; Default to maximum SX1262 drive
|
||||
-D MAX_LORA_TX_POWER=22 ; Max SX1262 output
|
||||
-D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX
|
||||
-D SX126X_DIO2_AS_RF_SWITCH=true ; GC1109 CTX is controlled by SX1262 DIO2
|
||||
@@ -104,6 +104,27 @@ lib_deps =
|
||||
${esp32_ota.lib_deps}
|
||||
bakercp/CRC32 @ ^2.0.0
|
||||
|
||||
[env:heltec_v4_repeater_legacy_partition_test]
|
||||
; Deliberately retains Arduino's 1.25 MiB OTA slots for validating the
|
||||
; Wi-Fi-only partition migration bridge on disposable hardware.
|
||||
extends = heltec_v4_oled
|
||||
board_build.partitions = default.csv
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D ADVERT_NAME='"Heltec Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
build_src_filter = ${heltec_v4_oled.build_src_filter}
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
lib_deps =
|
||||
${heltec_v4_oled.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
bakercp/CRC32 @ ^2.0.0
|
||||
|
||||
[env:heltec_v4_expansionkit_repeater]
|
||||
extends = heltec_v4_oled
|
||||
build_flags =
|
||||
@@ -828,3 +849,51 @@ build_flags =
|
||||
-DARDUINO_USB_MODE=1
|
||||
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
; One-time Wi-Fi-only migration bridge for legacy Heltec V4/V4.3 OLED
|
||||
; repeaters that still use Arduino's 1.25 MiB OTA slots. It is deliberately
|
||||
; linked against default.csv so the existing browser updater can install it.
|
||||
; On first boot it stages /identity/_main.id in unchanged NVS, makes a
|
||||
; CRC-verified copy of legacy SPIFFS, copies itself to app0 when necessary,
|
||||
; then publishes default_16MB. It restores the identity after the larger
|
||||
; SPIFFS region initializes and exposes a browser updater for the full image.
|
||||
[env:heltec_v4_partition_migrator]
|
||||
platform = platformio/espressif32@6.11.0
|
||||
board = heltec_v4_migrator
|
||||
framework = arduino
|
||||
board_build.partitions = default.csv
|
||||
; The V4's ROM/bootloader chain is reliable with DIO. The final Full image
|
||||
; uses the same setting, so there is no flash-mode transition after migration.
|
||||
board_build.flash_mode = dio
|
||||
; The partition-table erase disables the flash cache. Native USB CDC can still
|
||||
; raise a flash-backed event ISR during that interval, even after Serial.end().
|
||||
; The Wi-Fi-only bridge therefore keeps CDC disabled; the final regular
|
||||
; repeater build restores its normal USB serial setting.
|
||||
build_flags =
|
||||
-I variants/heltec_v4
|
||||
build_src_filter =
|
||||
-<*>
|
||||
+<../examples/esp32_partition_migrator>
|
||||
lib_deps =
|
||||
ESP32Async/AsyncTCP @ 3.5.0
|
||||
ESP32Async/ESPAsyncWebServer @ 3.12.0
|
||||
file://arch/esp32/AsyncElegantOTA
|
||||
|
||||
; Disposable-hardware harness: leaves a ten-minute Wi-Fi OTA window for
|
||||
; installing the production bridge into legacy slot B. This is not a release
|
||||
; profile.
|
||||
[env:heltec_v4_partition_migrator_test_hold]
|
||||
extends = env:heltec_v4_partition_migrator
|
||||
build_flags =
|
||||
${env:heltec_v4_partition_migrator.build_flags}
|
||||
-D MESHCORE_MIGRATION_DELAY_MS=600000
|
||||
-D MESHCORE_MIGRATION_RESTART_DELAY_MS=0
|
||||
|
||||
; Harness-only diagnostic: USB CDC is restored so a failed AP startup can be
|
||||
; distinguished from a board which never left its bootloader. It retains
|
||||
; the same ten-minute no-write window as test_hold.
|
||||
[env:heltec_v4_partition_migrator_test_usb_diagnostic]
|
||||
extends = env:heltec_v4_partition_migrator_test_hold
|
||||
build_flags =
|
||||
${env:heltec_v4_partition_migrator_test_hold.build_flags}
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
@@ -56,6 +56,56 @@ lib_deps =
|
||||
${Xiao_S3_WIO.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
|
||||
; Generic source, XIAO-specific board definition. It is intentionally linked
|
||||
; to default.csv so the stock legacy browser updater can install it.
|
||||
[env:xiao_s3_partition_migrator]
|
||||
platform = platformio/espressif32@6.11.0
|
||||
board = seeed_xiao_esp32s3_migrator
|
||||
framework = arduino
|
||||
board_build.partitions = default.csv
|
||||
board_build.flash_mode = dio
|
||||
build_flags =
|
||||
-I variants/xiao_s3_wio
|
||||
build_src_filter =
|
||||
-<*>
|
||||
+<../examples/esp32_partition_migrator>
|
||||
lib_deps =
|
||||
ESP32Async/AsyncTCP @ 3.5.0
|
||||
ESP32Async/ESPAsyncWebServer @ 3.12.0
|
||||
file://arch/esp32/AsyncElegantOTA
|
||||
|
||||
; Disposable-hardware seed. It reproduces a real legacy identity file and
|
||||
; exposes browser OTA without trying to squeeze a current full repeater into
|
||||
; its historical 1.25 MiB app slot.
|
||||
[env:xiao_s3_partition_legacy_seed]
|
||||
platform = platformio/espressif32@6.11.0
|
||||
board = seeed_xiao_esp32s3
|
||||
framework = arduino
|
||||
board_build.partitions = default.csv
|
||||
board_build.flash_mode = dio
|
||||
build_flags =
|
||||
-I src
|
||||
build_src_filter =
|
||||
-<*>
|
||||
+<../examples/esp32_partition_legacy_seed>
|
||||
lib_deps =
|
||||
ESP32Async/AsyncTCP @ 3.5.0
|
||||
ESP32Async/ESPAsyncWebServer @ 3.12.0
|
||||
file://arch/esp32/AsyncElegantOTA
|
||||
|
||||
[env:xiao_s3_partition_migrator_test_hold]
|
||||
extends = env:xiao_s3_partition_migrator
|
||||
build_flags =
|
||||
${env:xiao_s3_partition_migrator.build_flags}
|
||||
-D MESHCORE_MIGRATION_DELAY_MS=600000
|
||||
-D MESHCORE_MIGRATION_RESTART_DELAY_MS=0
|
||||
|
||||
[env:xiao_s3_partition_migrator_test_usb_diagnostic]
|
||||
extends = env:xiao_s3_partition_migrator_test_hold
|
||||
build_flags =
|
||||
${env:xiao_s3_partition_migrator_test_hold.build_flags}
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
; [env:Xiao_S3_WIO_repeater_bridge_rs232]
|
||||
; extends = Xiao_S3_WIO
|
||||
; build_src_filter = ${Xiao_S3_WIO.build_src_filter}
|
||||
|
||||
Reference in new issue
Block a user