From 72afac9a4008283ad7371fb45c87a1ded41a2eca Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 25 Sep 2026 10:35:04 -0700 Subject: [PATCH] Unify RAK repeater storage firmware and bootloader handoff --- build_legacy.sh | 24 ++- docs/_javascript/firmware_picker.js | 20 ++- platformio.ini | 18 +++ scripts/check_firmware_capabilities.py | 10 +- src/helpers/ManagementReporter.cpp | 7 +- src/helpers/StorageLayout.cpp | 16 +- src/helpers/ota/OtaApply.cpp | 41 ++++- src/helpers/ota/OtaBootloaderUpdate.h | 3 +- src/helpers/ota/OtaCli.cpp | 64 +++++++- src/helpers/ota/OtaContext.h | 34 +++- src/helpers/ota/OtaFlashLayout_nrf52.h | 1 + src/helpers/ota/OtaRakStoragePolicy.h | 39 +++++ src/helpers/ota/OtaStoreAdaptiveNrf52.h | 148 ++++++++++++++++++ src/helpers/ota/OtaStoreQspiNrf52.cpp | 100 ++++++++++++ src/helpers/ota/OtaStoreQspiNrf52.h | 6 + src/helpers/ota/OtaTargets.h | 24 ++- test/test_firmware_capabilities.py | 1 + test/test_firmware_picker.js | 27 ++++ test/test_rak_storage_policy.py | 64 ++++++++ tools/mota/motalib.py | 10 +- .../nrf52_internal_bootloader_targets.txt | 2 + tools/mota/pio_endf.py | 11 +- tools/mota/test_mota.py | 15 +- variants/rak3401/platformio.ini | 25 +++ variants/rak3401/variant.cpp | 5 + variants/rak4631/platformio.ini | 28 ++++ variants/rak4631/variant.cpp | 9 +- 27 files changed, 723 insertions(+), 29 deletions(-) create mode 100644 src/helpers/ota/OtaRakStoragePolicy.h create mode 100644 src/helpers/ota/OtaStoreAdaptiveNrf52.h create mode 100644 test/test_rak_storage_policy.py diff --git a/build_legacy.sh b/build_legacy.sh index 5a85e824..32bd0770 100755 --- a/build_legacy.sh +++ b/build_legacy.sh @@ -1985,6 +1985,15 @@ print_release_firmware_targets() { if is_supported_build_env "RAK_3401_repeater_rak13302_w25q16_lora_ota"; then printf '%s\n' "RAK_3401_repeater_rak13302_w25q16_lora_ota" fi + # One install image per RAK core now covers its matched internal and + # external staging choices. Older exact-identity targets remain as + # compatibility assets for nodes already installed in the field. + if is_supported_build_env "RAK_4631_repeater_unified_lora_ota"; then + printf '%s\n' "RAK_4631_repeater_unified_lora_ota" + fi + if is_supported_build_env "RAK_3401_repeater_unified_lora_ota"; then + printf '%s\n' "RAK_3401_repeater_unified_lora_ota" + fi # Functional consolidation does not change an installed image's mOTA # target ID. Keep exact Serial1/Serial2 bridge identities publishable as # compatibility assets while recommending the merged image for USB/new @@ -2857,7 +2866,11 @@ requires_dram_limited_neighbors() { # Keep the board's 50 entries in Full; retain its display and services. if [ "$ESP32_FULL_BUILD" = "1" ]; then return 0; fi ;; heltec_t096_repeater_lora_ota_no_external_sensors|\ - heltec_t1_repeater_lora_ota_no_external_sensors) + heltec_t1_repeater_lora_ota_no_external_sensors|\ + rak_3401_repeater_unified_lora_ota|\ + rak_4631_repeater_unified_lora_ota) + # Full sensors plus both OTA backends retain the board's original + # 50-neighbor table so the fixed mOTA RAM arena has usable heap. return 0 ;; generic_e22_sx1262_repeater_lora_ota_no_external_sensors|\ generic_e22_sx1268_repeater_lora_ota_no_external_sensors|\ @@ -3776,7 +3789,9 @@ apply_nrf52_lora_ota_build_recipe() { # and build.sh release builds cannot silently differ. supports_nrf52_internal_bootloader_update() { [ "${PIO_ENV_PLATFORM_BY_NAME[$1]:-}" = "NRF52_PLATFORM" ] || return 1 - [ "${PIO_ENV_QSPI_OTA_BY_NAME[$1]:-0}" = "0" ] || return 1 + if [ "${PIO_ENV_QSPI_OTA_BY_NAME[$1]:-0}" != "0" ]; then + pio_env_option_contains "$1" build_flags "OTA_RAK_AUTO_STORE" || return 1 + fi [ "${PIO_ENV_SD_OTA_BY_NAME[$1]:-0}" = "0" ] || return 1 is_lora_ota_build "$1" || return 1 grep -Fqx -- "$1" tools/mota/nrf52_internal_bootloader_targets.txt @@ -3793,7 +3808,10 @@ apply_lora_ota_override() { fi if is_lora_ota_build "$env_name"; then - if [ "${PIO_ENV_QSPI_OTA_BY_NAME[$env_name]:-0}" = "1" ]; then + if pio_env_option_contains "$env_name" build_flags "OTA_RAK_AUTO_STORE"; then + append_platformio_build_unflags "-UENABLE_OTA -DDISABLE_LORA_OTA=1" + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -DOTA_FLASH_STORE=1 -DOTA_QSPI_STORE=1 -DOTA_FOLDER_SERIAL" + elif [ "${PIO_ENV_QSPI_OTA_BY_NAME[$env_name]:-0}" = "1" ]; then append_platformio_build_unflags "-UENABLE_OTA -DDISABLE_LORA_OTA=1 -DOTA_FLASH_STORE=1 -DOTA_SD_STORE=1" export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UDISABLE_LORA_OTA -DENABLE_OTA=1 -UOTA_FLASH_STORE -UOTA_SD_STORE -DOTA_QSPI_STORE=1 -DOTA_FOLDER_SERIAL" elif [ "${PIO_ENV_SD_OTA_BY_NAME[$env_name]:-0}" = "1" ]; then diff --git a/docs/_javascript/firmware_picker.js b/docs/_javascript/firmware_picker.js index 12947512..f6714f26 100644 --- a/docs/_javascript/firmware_picker.js +++ b/docs/_javascript/firmware_picker.js @@ -377,6 +377,9 @@ ? "lora-source" : ""; let variant = variantForProfile(parts.role, parts.tail); + if (/^RAK_(?:3401|4631)_repeater_unified_lora_ota$/i.test(target)) { + variant = "default"; + } if (sourceHardware === "Station_G2_logging") { variant = variant === "default" ? "rx-boosted" @@ -528,6 +531,17 @@ }); } + function omitRakStorageProfilesWhenUnified(profiles) { + const targets = new Set((profiles || []).map(function (profile) { + return String(profile && profile.target || "").toLowerCase(); + })); + return (profiles || []).filter(function (profile) { + const target = String(profile && profile.target || "").toLowerCase(); + const match = /^rak_(3401|4631)_repeater_(?:lora_ota_no_external_sensors|w25q16_lora_ota|rak15001_slot_c_lora_ota|rak13302_w25q16_lora_ota)$/.exec(target); + return !match || !targets.has("rak_" + match[1] + "_repeater_unified_lora_ota"); + }); + } + function applyFullCompanionCapabilities(profiles) { return (profiles || []).map(function (profile) { if (!isFullCompanion(profile)) return profile; @@ -743,8 +757,10 @@ const profiles = omitLegacyObserverCompatibilityProfiles( applyMergedStandardUsbLoggingCapabilities( applyMergedRak4631RepeaterCapabilities( - omitTransportsReplacedByFull( - applyFullCompanionCapabilities(visibleProfiles) + omitRakStorageProfilesWhenUnified( + omitTransportsReplacedByFull( + applyFullCompanionCapabilities(visibleProfiles) + ) ) ) ) diff --git a/platformio.ini b/platformio.ini index 35879b69..6f6ceca4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -265,6 +265,24 @@ build_flags = -D OTA_QSPI_EXPECTED_JEDEC_ID=0xEF4015UL -D OTA_QSPI_EXPECTED_SIZE=2097152UL +; One RAK repeater image can use the RAK15001 (RAK4631 only), the separately +; wired W25Q16, or internal flash. The application probes exact JEDEC IDs and +; selects external staging only when the installed OTAFIX supports QSPI apply. +[nrf52_wisblock_auto_ota] +build_flags = + -D OTA_RAK_AUTO_STORE=1 + -D OTA_QSPI_STORE=1 + -D OTA_FLASH_STORE=1 + -D OTA_QSPI_SHARED_WISBLOCK_SPI=1 + -D OTA_QSPI_SCK_ARDUINO_PIN=3 + -D OTA_QSPI_CS_ARDUINO_PIN=31 + -D OTA_QSPI_IO0_ARDUINO_PIN=30 + -D OTA_QSPI_IO1_ARDUINO_PIN=29 + -D OTA_QSPI_IO2_NOT_CONNECTED=1 + -D OTA_QSPI_IO3_NOT_CONNECTED=1 + -D OTA_QSPI_SCK_FREQUENCY=NRF_QSPI_FREQ_32MDIV4 + -D OTA_QSPI_EXPECTED_SIZE=2097152UL + ; Compatibility alias retained for existing nRF52840 board definitions. [rak4631_hw] extends = nrf52_lora_ota diff --git a/scripts/check_firmware_capabilities.py b/scripts/check_firmware_capabilities.py index d3055c8f..f5a6e788 100644 --- a/scripts/check_firmware_capabilities.py +++ b/scripts/check_firmware_capabilities.py @@ -24,13 +24,17 @@ def nrf52_lora_details(application): raise ValueError("nRF52 LoRa OTA requires a valid EndF and storage-layout record") if len(application) > layout.linked_app_end - layout.app_base: raise ValueError("nRF52 LoRa OTA application exceeds its linked flash region") - storage = ("external_qspi" if layout.qspi_backed else "external_sd" if layout.sd_backed + storage = ("adaptive_internal_or_external_qspi" if layout.auto_store + else "external_qspi" if layout.qspi_backed else "external_sd" if layout.sd_backed else "internal_flash_and_retained_ram" if layout.hybrid_ram else "internal_flash") notes = ["Use a destination-specific .mota package matching the board, target, and storage layout.", "An in-place delta requires the exact firmware currently running as its base.", "The package must fit the receiver's staging and apply workspace.", "Artifact inspection verifies compiled support; it does not verify the bootloader installed on a physical device."] - if layout.hybrid_ram: + if layout.auto_store: + notes.append("One application detects RAK external NOR and chooses QSPI only with the exact matching OTAFIX bootloader; otherwise it uses internal flash where safe.") + notes.append("External QSPI accepts full images and in-place deltas; internal flash accepts in-place deltas only and needs the retained-RAM OTAFIX profile.") + elif layout.hybrid_ram: notes.append("Requires OTAFIX 2.4.6 retained-RAM handoff support; a transfer cannot resume after the receiver restarts.") elif layout.external_backed: notes.append("Requires the matching external-storage hardware, wiring, and storage-aware OTAFIX bootloader.") @@ -38,7 +42,7 @@ def nrf52_lora_details(application): notes.append("Internal storage accepts in-place application deltas, not full application packages.") return { "storage": storage, - "package_types": ["full", "in_place_delta"] if layout.external_backed else ["in_place_delta"], + "package_types": ["full", "in_place_delta"] if layout.auto_store or layout.external_backed else ["in_place_delta"], "bootloader": "Matching board/storage OTAFIX bootloader", "bootloader_release": "https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/0.11.0-OTAFIX2.4.6", "notes": notes, diff --git a/src/helpers/ManagementReporter.cpp b/src/helpers/ManagementReporter.cpp index 678aa5b2..e3b74c19 100644 --- a/src/helpers/ManagementReporter.cpp +++ b/src/helpers/ManagementReporter.cpp @@ -149,7 +149,12 @@ void ManagementReporter::snapshot() { } #elif defined(NRF52_PLATFORM) const auto bl = ota::ota_bootloader_app_caps(); -#if defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE) +#if defined(OTA_RAK_AUTO_STORE) + const auto* active_context = ota::ota_context_if_active(); + const uint16_t codecs = bl.codec_mask & + (!active_context ? 4u : active_context->fetch_store.usesExternal() ? 5u : + active_context->fetch_store.usesInternal() ? 4u : 0u); +#elif defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE) const uint16_t codecs = bl.codec_mask & 5u; // full + in-place #else const uint16_t codecs = bl.codec_mask & 4u; // internal app path accepts deltas only diff --git a/src/helpers/StorageLayout.cpp b/src/helpers/StorageLayout.cpp index 859b0573..c990821c 100644 --- a/src/helpers/StorageLayout.cpp +++ b/src/helpers/StorageLayout.cpp @@ -76,7 +76,21 @@ void formatStorageLayout(MainBoard& board, char* reply, size_t reply_size) { (unsigned long)internal_fs_start, (unsigned long)(internal_fs_size / 1024UL)); -#if defined(ENABLE_OTA) && defined(OTA_QSPI_STORE) +#if defined(ENABLE_OTA) && defined(OTA_RAK_AUTO_STORE) + (void)board; + mesh::ota::OtaStoreAdaptiveNrf52& store = mesh::ota::ota_ctx().fetch_store; + if (store.usesExternal()) { + const uint32_t capacity = store.qspiCapacity(); + appendStorageLayout(reply, reply_size, + "; ota=%s qspi=%luK id=%06lX", + store.selectionReason(), (unsigned long)(capacity / 1024UL), + (unsigned long)store.jedec_id()); + } else { + appendStorageLayout(reply, reply_size, "; ota=%s internal=%luK", + store.selectionReason(), + (unsigned long)(store.capacity() / 1024UL)); + } +#elif defined(ENABLE_OTA) && defined(OTA_QSPI_STORE) (void)board; mesh::ota::OtaStoreQspiNrf52& store = mesh::ota::ota_ctx().fetch_store; const uint32_t capacity = store.capacity(); diff --git a/src/helpers/ota/OtaApply.cpp b/src/helpers/ota/OtaApply.cpp index 23e2bedb..d5c91b6c 100644 --- a/src/helpers/ota/OtaApply.cpp +++ b/src/helpers/ota/OtaApply.cpp @@ -465,6 +465,9 @@ bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, Ap // not be published until the command reply has drained and reset is imminent. static OtaStoreFlashNrf52* g_nrf52_apply_store = nullptr; #endif +#if defined(OTA_QSPI_STORE) +static uint8_t g_nrf52_qspi_handoff = GPREGRET2_OTA_STAGE_QSPI; +#endif static void ota_nrf52_set_reset_handoff(uint8_t request, uint8_t source) { uint8_t sd_en = 0; @@ -494,10 +497,26 @@ static bool ota_nrf52_clear_reset_reasons() { void ota_reboot_to_apply() { // public: set the apply magic + reset (does not return) uint8_t stage_handoff = GPREGRET2_OTA_STAGE_LEGACY; -#if defined(OTA_SD_STORE) +#if defined(OTA_RAK_AUTO_STORE) + if (g_nrf52_apply_store) { + if (g_nrf52_apply_store->is_hybrid()) { + if (!ota_nrf52_clear_reset_reasons() || + !g_nrf52_apply_store->publish_hybrid_handoff()) { + ota_nrf52_set_reset_handoff(0u, 0xBDu); + NVIC_SystemReset(); + return; + } + stage_handoff = GPREGRET2_OTA_STAGE_HYBRID; + } else { + stage_handoff = mota_nrf52_flash_stage_handoff(ota_nrf52_effective_stage_ceiling()); + } + } else { + stage_handoff = g_nrf52_qspi_handoff; + } +#elif defined(OTA_SD_STORE) stage_handoff = GPREGRET2_OTA_STAGE_SD; #elif defined(OTA_QSPI_STORE) - stage_handoff = GPREGRET2_OTA_STAGE_QSPI; + stage_handoff = g_nrf52_qspi_handoff; #elif defined(OTA_FLASH_STORE) if (g_nrf52_apply_store && g_nrf52_apply_store->is_hybrid()) { // The bootloader admits retained SRAM only after a clean software-reset @@ -1171,7 +1190,23 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreSdNrf52& store, #if defined(OTA_QSPI_STORE) bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store, const SignerAllowlist& allow, ApplyState& st, char* msg) { - return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", false, st, msg); +#if defined(OTA_RAK_AUTO_STORE) + g_nrf52_apply_store = nullptr; +#endif + g_nrf52_qspi_handoff = GPREGRET2_OTA_STAGE_QSPI; + if (!ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", false, st, msg)) { + return false; + } +#if defined(OTA_RAK_AUTO_STORE) && !defined(RAK_3401) + if (store.jedec_id() == 0xC84015UL) { + OtaBootloaderIdentity identity; + if (ota_installed_bootloader_identity(identity) && identity.crc_ok && + strcmp(identity.device_name, "4631_AUTO_DFU") == 0) { + g_nrf52_qspi_handoff = GPREGRET2_OTA_STAGE_RAK15001; + } + } +#endif + return true; } #if defined(OTA_QSPI_BOOTLOADER_UPDATE) diff --git a/src/helpers/ota/OtaBootloaderUpdate.h b/src/helpers/ota/OtaBootloaderUpdate.h index c1995e9e..a5adfccb 100644 --- a/src/helpers/ota/OtaBootloaderUpdate.h +++ b/src/helpers/ota/OtaBootloaderUpdate.h @@ -26,7 +26,8 @@ #if !defined(NRF52_PLATFORM) || !defined(OTA_FLASH_STORE) #error "OTA_INTERNAL_BOOTLOADER_UPDATE requires nRF52 internal-flash staging" #endif - #if defined(OTA_QSPI_STORE) || defined(OTA_SD_STORE) || defined(QSPIFLASH) + #if (defined(OTA_QSPI_STORE) && !defined(OTA_RAK_AUTO_STORE)) || \ + defined(OTA_SD_STORE) || defined(QSPIFLASH) #error "internal bootloader staging cannot share an external OTA/filesystem store" #endif #endif diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index 6134f80d..af2aa03d 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -181,6 +181,9 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board "OTA: status | stats | ls | get flash [rescue] | install | rescue install | " "cancel | announce | self | folder | config | key"); #endif +#elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) + strcpy(reply, + "OTA: status | stats | ls | get flash | install | bootloader | cancel | announce | self | storage | folder | config | key"); #elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) #if defined(OTA_QSPI_BOOTLOADER_UPDATE) strcpy(reply, @@ -244,7 +247,11 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board // nRF52 applies via the bootloader - show (cached) whether it can, so `ota get`/`install` won't surprise. // blrc = the bootloader's last apply code (diagnostic; 0xB8=success, see ota_delta.c). const OtaBlCaps& bl = c.bootloaderAppCaps(); -#if defined(OTA_QSPI_STORE) +#if defined(OTA_RAK_AUTO_STORE) + const char* bl_state = !bl.present ? "NONE" : + c.fetch_store.usesExternal() ? "QSPI" : + c.fetch_store.usesInternal() ? "internal" : "storage-ERR"; +#elif defined(OTA_QSPI_STORE) const char* bl_state = !bl.present ? "NONE" : (bl.storage_flags & OTA_BL_STORAGE_QSPI) ? "QSPI" : "NO-QSPI"; #elif defined(OTA_SD_STORE) @@ -348,7 +355,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board #endif #endif #if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \ - !defined(OTA_QSPI_STORE) + (!defined(OTA_QSPI_STORE) || defined(OTA_RAK_AUTO_STORE)) SelfFwInfo list_self; bool list_has_endf = ota_self_firmware(list_self) && list_self.valid; #endif @@ -407,6 +414,11 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board && h->codec < 16 && (list_bl.codec_mask & (1u << h->codec)); #if defined(OTA_SD_STORE) installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_SD); +#elif defined(OTA_RAK_AUTO_STORE) + if (c.fetch_store.usesExternal()) + installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_QSPI); + if (h->codec == CODEC_FULL) + installable = installable && c.fetch_store.usesExternal(); #elif defined(OTA_QSPI_STORE) installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_QSPI); #endif @@ -584,6 +596,15 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board strcpy(reply, "ERR bootloader cannot apply an update staged on SD; update it over USB first"); return true; } +#elif defined(OTA_RAK_AUTO_STORE) + if (c.fetch_store.usesExternal() && !(bl.storage_flags & OTA_BL_STORAGE_QSPI)) { + strcpy(reply, "ERR bootloader cannot apply from detected QSPI storage"); + return true; + } + if (!c.fetch_store.usesExternal() && !c.fetch_store.usesInternal()) { + snprintf(reply, 160, "ERR OTA storage unsafe: %s", c.fetch_store.selectionReason()); + return true; + } #elif defined(OTA_QSPI_STORE) if (!(bl.storage_flags & OTA_BL_STORAGE_QSPI)) { strcpy(reply, "ERR bootloader cannot apply an update staged on QSPI; update it over USB first"); @@ -592,7 +613,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board #endif #endif #if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \ - !defined(OTA_QSPI_STORE) + (!defined(OTA_QSPI_STORE) || defined(OTA_RAK_AUTO_STORE)) SelfFwInfo self; bool has_endf = ota_self_firmware(self) && self.valid; #if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) @@ -667,7 +688,9 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board if (was_folder && c.folder_dest) c.folder_dest->clear(); c.fetch_to_folder = false; c.manager.set_fetch_store(&c.fetch_store); // revert to the default flash store (a folder pull switched it) -#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE) && !defined(OTA_QSPI_STORE) +#if defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) + c.manager.set_accept_full(c.fetch_store.usesExternal()); +#elif defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE) && !defined(OTA_QSPI_STORE) c.manager.set_accept_full(false); #endif const bool discarded = !was_folder && !was_sd_archive && @@ -710,7 +733,21 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board // ---- raw-QSPI staging diagnostics (read-only probe; preserves a latched fetch failure) ---- } else if (is_cmd(a, "qspi|storage", &rest)) { -#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) +#if defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) + if (c.fetch_store.usesExternal()) { + const uint32_t qspi_capacity = c.fetch_store.qspiCapacity(); + snprintf(reply, 160, "QSPI %s jedec=%06lX size=%luK sr1=%02X stage=%s%s%s", + c.fetch_store.selectionReason(), (unsigned long)c.fetch_store.jedec_id(), + (unsigned long)(qspi_capacity / 1024), c.fetch_store.status1(), + c.fetch_store.last_stage(), c.fetch_store.last_error()[0] ? " error=" : "", + c.fetch_store.last_error()); + } else if (c.fetch_store.usesInternal()) { + snprintf(reply, 160, "OTA storage: %s; internal capacity=%luK", + c.fetch_store.selectionReason(), (unsigned long)(c.fetch_store.capacity() / 1024)); + } else { + snprintf(reply, 160, "OTA storage unsafe: %s", c.fetch_store.selectionReason()); + } +#elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) // This probe only reads JEDEC/SR1. capacity() deliberately preserves a // latched fetch failure so asking for diagnostics cannot erase its cause. uint32_t qspi_capacity = c.fetch_store.capacity(); @@ -735,7 +772,22 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board #if defined(NRF52_PLATFORM) // nRF52 applies via the bootloader, so surface whether THIS device's bootloader can install this store. const OtaBlCaps& bl = c.bootloaderAppCaps(); // cached (flash scanned once) -#if defined(OTA_QSPI_STORE) +#if defined(OTA_RAK_AUTO_STORE) + if (c.fetch_store.usesExternal()) { + const uint32_t qspi_capacity = c.fetch_store.qspiCapacity(); + snprintf(reply + n, 160 - n, " | QSPI store:%s%uK | bootloader: %s", + qspi_capacity ? "" : "ERR ", + (unsigned)(qspi_capacity / 1024), + (bl.present && (bl.storage_flags & OTA_BL_STORAGE_QSPI)) ? "QSPI apply OK" : "NO QSPI apply"); + } else if (c.fetch_store.usesInternal()) { + snprintf(reply + n, 160 - n, " | internal store:%luK | bootloader: %s", + (unsigned long)(c.fetch_store.capacity() / 1024), + bl.present ? "delta apply OK" : "NO delta apply"); + } else { + snprintf(reply + n, 160 - n, " | storage unsafe: %s", + c.fetch_store.selectionReason()); + } +#elif defined(OTA_QSPI_STORE) uint32_t qspi_capacity = c.fetch_store.capacity(); n += snprintf(reply + n, 160 - n, " | QSPI store:%s%uK", qspi_capacity ? "" : "ERR ", (unsigned)(qspi_capacity / 1024)); diff --git a/src/helpers/ota/OtaContext.h b/src/helpers/ota/OtaContext.h index 3c4acd4f..1698ab13 100644 --- a/src/helpers/ota/OtaContext.h +++ b/src/helpers/ota/OtaContext.h @@ -26,7 +26,9 @@ #define OTA_DYNAMIC_CONTEXT 0 #endif -#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) +#if defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) + #include "OtaStoreAdaptiveNrf52.h" +#elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) #include "OtaStoreQspiNrf52.h" #elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) #include "OtaStoreSdNrf52.h" @@ -107,6 +109,8 @@ struct OtaContext { // store object for OtaManager, while folder captures replace it with the // host-backed FolderMotaStore for the duration of the pull. OtaStoreRam<1> fetch_store; +#elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) + OtaStoreAdaptiveNrf52 fetch_store; #elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) OtaStoreQspiNrf52 fetch_store; // persistent raw QSPI staging, full + in-place delta #elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) @@ -305,7 +309,15 @@ struct OtaContext { } } bool ok; -#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)) +#if defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) + ok = fetch_store.usesExternal() + ? ota_apply_mota_nrf52(fetch_store.externalStore(), allow, apply_st, msg) + : fetch_store.usesInternal() + ? ota_apply_mota_nrf52(fetch_store.internalStore(), allow, apply_st, msg) + : false; + if (!fetch_store.usesExternal() && !fetch_store.usesInternal()) + strncpy(msg, fetch_store.selectionReason(), 96); +#elif defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)) ok = ota_apply_mota_nrf52(fetch_store, allow, apply_st, msg); #elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) if (rescue_base_hash) { @@ -346,9 +358,19 @@ struct OtaContext { msg[95] = 0; return false; } const OtaBootloaderIdentity& installed = bootloaderIdentity(); +#if defined(OTA_RAK_AUTO_STORE) + if (!fetch_store.usesInternal()) { + strncpy(msg, "bootloader LoRa update needs the internal OTAFIX profile", 96); + msg[95] = 0; return false; + } + bool ok = ota_prepare_bootloader_update_nrf52( + fetch_store.internalStore(), allow, installed, manager.fetchManifestId(), operator_mid, + operator_hash8, apply_st, msg); +#else bool ok = ota_prepare_bootloader_update_nrf52( fetch_store, allow, installed, manager.fetchManifestId(), operator_mid, operator_hash8, apply_st, msg); +#endif if (ok) { bootloader_apply_pending = true; apply_pending = true; } return ok; #else @@ -664,6 +686,9 @@ struct OtaContext { manager.set_accept_full(true); manager.set_autofetch(OtaManager::AUTOFETCH_OFF); autoinstall = AUTOINSTALL_OFF; +#elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) + manager.set_accept_full(fetch_store.usesExternal()); + manager.set_apply_codec(CODEC_DETOOLS_INPLACE); #elif defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)) manager.set_accept_full(true); manager.set_apply_codec(CODEC_DETOOLS_INPLACE); @@ -678,7 +703,12 @@ struct OtaContext { #if defined(NRF52_PLATFORM) && \ (defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \ defined(OTA_SD_BOOTLOADER_UPDATE)) +#if defined(OTA_RAK_AUTO_STORE) + manager.set_accept_bootloader(fetch_store.usesInternal() && + ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps())); +#else manager.set_accept_bootloader(true); +#endif #else manager.set_accept_bootloader(false); #endif diff --git a/src/helpers/ota/OtaFlashLayout_nrf52.h b/src/helpers/ota/OtaFlashLayout_nrf52.h index 640606d3..6ca53610 100644 --- a/src/helpers/ota/OtaFlashLayout_nrf52.h +++ b/src/helpers/ota/OtaFlashLayout_nrf52.h @@ -55,6 +55,7 @@ static const uint8_t GPREGRET_OTA_BOOTLOADER_UPDATE = 0x6Bu; static const uint8_t GPREGRET2_OTA_STAGE_LEGACY = 0xD4u; static const uint8_t GPREGRET2_OTA_STAGE_EXPANDED = 0xEDu; static const uint8_t GPREGRET2_OTA_STAGE_QSPI = 0x51u; +static const uint8_t GPREGRET2_OTA_STAGE_RAK15001 = 0x52u; static const uint8_t GPREGRET2_OTA_STAGE_SD = 0x53u; static const uint8_t GPREGRET2_OTA_STAGE_HYBRID = 0xA6u; diff --git a/src/helpers/ota/OtaRakStoragePolicy.h b/src/helpers/ota/OtaRakStoragePolicy.h new file mode 100644 index 00000000..2eb573d0 --- /dev/null +++ b/src/helpers/ota/OtaRakStoragePolicy.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +namespace mesh { +namespace ota { + +enum class RakStorageChoice : uint8_t { Internal, Qspi, Unsafe }; + +// detected: 0 = absent, 1 = RAK15001 C, 2 = W25Q16, 3 = both. +// A QSPI bootloader can only apply from the chip and wiring it was built for. +// Never stage into a different NOR just because it answered a JEDEC query. +inline RakStorageChoice rak_storage_choice(uint8_t detected, + bool qspi_bootloader, + bool identity_valid, + const char* device_name, + bool rak3401) { + if (detected > 3u || detected == 3u) return RakStorageChoice::Unsafe; + const char* auto_name = rak3401 ? "3401_AUTO_DFU" : "4631_AUTO_DFU"; + const bool merged_bootloader = identity_valid && device_name && + strcmp(device_name, auto_name) == 0; + if (detected == 0u) return !qspi_bootloader || merged_bootloader + ? RakStorageChoice::Internal : RakStorageChoice::Unsafe; + if (!qspi_bootloader) return RakStorageChoice::Internal; + if (!identity_valid || !device_name) return RakStorageChoice::Unsafe; + if (merged_bootloader) { + return rak3401 && detected == 1u ? RakStorageChoice::Unsafe + : RakStorageChoice::Qspi; + } + const char* expected = rak3401 + ? (detected == 2u ? "3401_W25Q16_DFU" : "") + : (detected == 1u ? "4631_15001C_DFU" : "4631_W25Q16_DFU"); + return expected[0] && strcmp(device_name, expected) == 0 + ? RakStorageChoice::Qspi : RakStorageChoice::Unsafe; +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaStoreAdaptiveNrf52.h b/src/helpers/ota/OtaStoreAdaptiveNrf52.h new file mode 100644 index 00000000..34761691 --- /dev/null +++ b/src/helpers/ota/OtaStoreAdaptiveNrf52.h @@ -0,0 +1,148 @@ +#pragma once + +#if defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE) + +#include "OtaBlInfo.h" +#include "OtaApply.h" +#include "OtaBootloaderUpdate.h" +#include "OtaStoreFlashNrf52.h" +#include "OtaStoreQspiNrf52.h" +#include "OtaRakStoragePolicy.h" +#include + +namespace mesh { +namespace ota { + +// One RAK application image supports the board's internal OTA store and its +// explicitly matched external NOR. Selection is fixed for the lifetime of an +// app boot; no staged container is ever moved between backends. +class OtaStoreAdaptiveNrf52 : public OtaStore { + enum Mode : uint8_t { UNSELECTED, INTERNAL_MODE, QSPI_MODE, UNSAFE }; + mutable Mode _mode = UNSELECTED; + mutable const char* _reason = "not probed"; + // Only one backend is usable for a given board/bootloader pairing. Sharing + // their two flash-page buffers saves 8 KiB of scarce nRF52840 runtime RAM. + union StoreStorage { + OtaStoreFlashNrf52 internal; + OtaStoreQspiNrf52 external; + StoreStorage() {} + ~StoreStorage() {} + }; + mutable StoreStorage _storage; + + void activateInternal() const { + new (&_storage.internal) OtaStoreFlashNrf52(); + _mode = INTERNAL_MODE; + } + void activateExternal() const { + new (&_storage.external) OtaStoreQspiNrf52(); + _mode = QSPI_MODE; + } + + void select() const { + if (_mode != UNSELECTED) return; + const uint8_t detected = OtaStoreQspiNrf52::autoDetect(); + const OtaBlCaps caps = ota_bootloader_app_caps(); + if (detected == 3u) { + _mode = UNSAFE; + _reason = "both external NOR types detected"; + return; + } + const bool qspi_bootloader = caps.present && + (caps.storage_flags & OTA_BL_STORAGE_QSPI) != 0; + OtaBootloaderIdentity identity; + const bool identity_valid = qspi_bootloader && + ota_installed_bootloader_identity(identity) && identity.crc_ok; +#if defined(RAK_3401) + const bool rak3401 = true; +#else + const bool rak3401 = false; +#endif + const RakStorageChoice choice = rak_storage_choice( + detected, qspi_bootloader, identity_valid, + identity_valid ? identity.device_name : nullptr, rak3401); + if (choice == RakStorageChoice::Internal) { + activateInternal(); + _reason = detected == 0u ? "no external NOR" : + "external NOR fitted; internal bootloader"; + return; + } + if (choice == RakStorageChoice::Qspi) { + activateExternal(); + _reason = detected == 1u ? "RAK15001 C" : "W25Q16"; + return; + } + _mode = UNSAFE; + if (detected == 0u) { + _reason = "QSPI bootloader but no matched external NOR"; + } else if (!identity_valid) { + _reason = "QSPI bootloader identity unavailable"; + } else { + _reason = "external NOR and OTAFIX bootloader do not match"; + } + } + + OtaStore* active() { + select(); + return _mode == QSPI_MODE ? static_cast(&_storage.external) : + _mode == INTERNAL_MODE ? static_cast(&_storage.internal) : nullptr; + } + const OtaStore* active() const { + select(); + return _mode == QSPI_MODE ? static_cast(&_storage.external) : + _mode == INTERNAL_MODE ? static_cast(&_storage.internal) : nullptr; + } + +public: + ~OtaStoreAdaptiveNrf52() override { + if (_mode == QSPI_MODE) _storage.external.~OtaStoreQspiNrf52(); + else if (_mode == INTERNAL_MODE) _storage.internal.~OtaStoreFlashNrf52(); + } + bool usesExternal() const { select(); return _mode == QSPI_MODE; } + bool usesInternal() const { select(); return _mode == INTERNAL_MODE; } + const char* selectionReason() const { select(); return _reason; } + OtaStoreFlashNrf52& internalStore() { select(); return _storage.internal; } + OtaStoreQspiNrf52& externalStore() { select(); return _storage.external; } + uint32_t qspiCapacity() const { return usesExternal() ? _storage.external.capacity() : 0u; } + uint32_t jedec_id() const { return usesExternal() ? _storage.external.jedec_id() : 0u; } + uint8_t status1() const { return usesExternal() ? _storage.external.status1() : 0xFFu; } + const char* last_stage() const { return usesExternal() ? _storage.external.last_stage() : "inactive"; } + const char* last_error() const { + return usesExternal() ? _storage.external.last_error() : selectionReason(); + } + + bool begin(uint32_t n) override { OtaStore* s = active(); return s && s->begin(n); } + bool write(uint32_t p, const uint8_t* d, uint32_t n) override { + OtaStore* s = active(); return s && s->write(p, d, n); + } + bool read(uint32_t p, uint8_t* d, uint32_t n) const override { + const OtaStore* s = active(); return s && s->read(p, d, n); + } + uint32_t capacity() const override { + const OtaStore* s = active(); return s ? s->capacity() : 0u; + } + uint32_t staged_size() const override { + const OtaStore* s = active(); return s ? s->staged_size() : 0u; + } + void clear() override { OtaStore* s = active(); if (s) s->clear(); } + bool discard() override { OtaStore* s = active(); return s && s->discard(); } + bool set_meta_size(uint32_t n) override { + OtaStore* s = active(); return s && s->set_meta_size(n); + } + bool finalize() override { OtaStore* s = active(); return s && s->finalize(); } + void checkpoint() override { OtaStore* s = active(); if (s) s->checkpoint(); } + bool reopen() override { OtaStore* s = active(); return s && s->reopen(); } + bool reopenFor(const uint8_t* mid, uint32_t target) override { + OtaStore* s = active(); return s && s->reopenFor(mid, target); + } + bool plan_layout(bool full, uint32_t image, uint32_t payload_off, + uint32_t payload_size, bool bootloader) override { + OtaStore* s = active(); + return s && s->plan_layout(full, image, payload_off, payload_size, bootloader); + } +}; + +} // namespace ota +} // namespace mesh + +#endif diff --git a/src/helpers/ota/OtaStoreQspiNrf52.cpp b/src/helpers/ota/OtaStoreQspiNrf52.cpp index d0ee842c..08b5c3a7 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.cpp +++ b/src/helpers/ota/OtaStoreQspiNrf52.cpp @@ -19,6 +19,12 @@ namespace ota { namespace { +#if defined(OTA_RAK_AUTO_STORE) +static uint8_t auto_cs_pin = NRF_QSPI_PIN_NOT_CONNECTED; +static uint32_t auto_jedec_id = 0; +static uint8_t auto_detection = 0xFF; +#endif + #if defined(OTA_QSPI_SCK_PHYSICAL_PIN) && defined(OTA_QSPI_SCK_ARDUINO_PIN) #error "QSPI SCK must use either a physical or Arduino pin override" #elif !defined(OTA_QSPI_SCK_PHYSICAL_PIN) && !defined(OTA_QSPI_SCK_ARDUINO_PIN) @@ -100,11 +106,15 @@ static uint8_t qspi_sck_pin() { } static uint8_t qspi_cs_pin() { +#if defined(OTA_RAK_AUTO_STORE) + return auto_cs_pin; +#else #ifdef OTA_QSPI_CS_PHYSICAL_PIN return OTA_QSPI_CS_PHYSICAL_PIN; #else return arduino_to_physical(OTA_QSPI_CS_ARDUINO_PIN); #endif +#endif } static uint8_t qspi_io0_pin() { @@ -243,8 +253,90 @@ static void wake_qspi_flash_before_activate(const nrf_qspi_pins_t& pins) { delayMicroseconds(MOTA_QSPI_DPD_WAKE_GUARD_US); } +#if defined(OTA_RAK_AUTO_STORE) +static uint8_t gpio_spi_byte(const nrf_qspi_pins_t& pins, uint8_t out) { + uint8_t in = 0; + for (uint8_t bit = 0; bit < 8u; ++bit) { + nrf_gpio_pin_write(pins.io0_pin, (out & 0x80u) != 0); + out <<= 1; + delayMicroseconds(1); + nrf_gpio_pin_set(pins.sck_pin); + delayMicroseconds(1); + in = (uint8_t)((in << 1) | nrf_gpio_pin_read(pins.io1_pin)); + nrf_gpio_pin_clear(pins.sck_pin); + delayMicroseconds(1); + } + return in; +} + +static uint32_t probe_nor(uint8_t cs) { + const nrf_qspi_pins_t pins = { + qspi_sck_pin(), cs, qspi_io0_pin(), qspi_io1_pin(), + qspi_io2_pin(), qspi_io3_pin() + }; + // A previous reset may have left the NOR in deep power-down. This GPIO + // wake precedes the nRF QSPI ACTIVATE for the same reason as ensureFlash(). + wake_qspi_flash_before_activate(pins); + nrf_gpio_pin_clear(cs); + delayMicroseconds(1); + (void)gpio_spi_byte(pins, 0x9Fu); + const uint32_t id = ((uint32_t)gpio_spi_byte(pins, 0xFFu) << 16) | + ((uint32_t)gpio_spi_byte(pins, 0xFFu) << 8) | + gpio_spi_byte(pins, 0xFFu); + nrf_gpio_pin_set(cs); + delayMicroseconds(1); + if (id == 0xC84015UL || id == 0xEF4015UL) { + // Detection is read-only. Return a recognized NOR to low-power standby; + // ensureFlash() issues its own 0xAB before taking over with QSPI. + nrf_gpio_pin_clear(cs); + delayMicroseconds(1); + (void)gpio_spi_byte(pins, 0xB9u); + nrf_gpio_pin_set(cs); + delayMicroseconds(MOTA_QSPI_DPD_ENTRY_GUARD_US); + } + return id; +} +#endif + } // namespace +#if defined(OTA_RAK_AUTO_STORE) +uint8_t OtaStoreQspiNrf52::autoDetect() { + if (auto_detection != 0xFFu) return auto_detection; + + const uint8_t w25_cs = arduino_to_physical(31); + nrf_gpio_pin_set(w25_cs); + nrf_gpio_cfg_output(w25_cs); +#if defined(RAK_3401) + // RAK13302 shares clock/data with the flash. Its NSS must stay high while + // the GPIO probe owns those signals, then SPI1 is restored for the radio. + pinMode(P_LORA_NSS, OUTPUT); + digitalWrite(P_LORA_NSS, HIGH); + SPI1.end(); + const uint32_t w25_id = probe_nor(w25_cs); + SPI1.begin(); + auto_detection = w25_id == 0xEF4015UL ? 2u : 0u; +#else + const uint8_t rak_cs = arduino_to_physical(26); + nrf_gpio_pin_set(rak_cs); + nrf_gpio_cfg_output(rak_cs); + const uint32_t rak_id = probe_nor(rak_cs); + const uint32_t w25_id = probe_nor(w25_cs); + const bool rak = rak_id == 0xC84015UL; + const bool w25 = w25_id == 0xEF4015UL; + auto_detection = rak && w25 ? 3u : rak ? 1u : w25 ? 2u : 0u; +#endif + if (auto_detection == 1u) { + auto_cs_pin = arduino_to_physical(26); + auto_jedec_id = 0xC84015UL; + } else if (auto_detection == 2u) { + auto_cs_pin = w25_cs; + auto_jedec_id = 0xEF4015UL; + } + return auto_detection; +} +#endif + OtaStoreQspiNrf52::OtaStoreQspiNrf52() { resetSession(); } @@ -442,6 +534,14 @@ bool OtaStoreQspiNrf52::ensureFlash() { fail("QSPI JEDEC ID does not match target"); return false; } +#endif +#if defined(OTA_RAK_AUTO_STORE) + if (auto_detection == 0xFFu || auto_detection == 0u || + auto_detection == 3u || _jedec_id != auto_jedec_id) { + releaseFlash(); + fail("QSPI JEDEC ID does not match detected RAK storage"); + return false; + } #endif _flash_size = 1UL << jedec[2]; if (_flash_size > MAX_FLASH) { diff --git a/src/helpers/ota/OtaStoreQspiNrf52.h b/src/helpers/ota/OtaStoreQspiNrf52.h index d7a8fbd6..09873db9 100644 --- a/src/helpers/ota/OtaStoreQspiNrf52.h +++ b/src/helpers/ota/OtaStoreQspiNrf52.h @@ -166,6 +166,12 @@ public: OtaStoreQspiNrf52(); ~OtaStoreQspiNrf52() override; +#if defined(OTA_RAK_AUTO_STORE) + // Probe exact supported NOR parts before activating the nRF QSPI block. + // 0 = absent/unsupported, 1 = RAK15001 C, 2 = W25Q16, 3 = ambiguous. + static uint8_t autoDetect(); +#endif + bool begin(uint32_t total_size) override; bool write(uint32_t offset, const uint8_t *data, uint32_t len) override; bool read(uint32_t offset, uint8_t *buf, uint32_t len) const override; diff --git a/src/helpers/ota/OtaTargets.h b/src/helpers/ota/OtaTargets.h index ba795417..39eadb49 100644 --- a/src/helpers/ota/OtaTargets.h +++ b/src/helpers/ota/OtaTargets.h @@ -2,7 +2,7 @@ #include // AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand. -// 612 OTA-capable build targets. Maps target_id (= sha2-256:4 of the target name, LE uint32) +// 634 OTA-capable build targets. Maps target_id (= sha2-256:4 of the target name, LE uint32) // to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT // transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes. // Size-constrained receivers can set OTA_TARGET_NAME_TABLE=0. They still match targets by ID; @@ -47,6 +47,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x875f3744, "Generic_ESPNOW_repeatr" }, { 0xdaf349be, "Generic_ESPNOW_room_svr" }, { 0x93b93199, "Generic_ESPNOW_terminal_chat" }, + { 0x710abb59, "GEPRC_Linkflow_900_repeater" }, { 0x5293da13, "Heltec_ct62_companion_radio_ble" }, { 0xc6dd7723, "Heltec_ct62_companion_radio_ble_ps" }, { 0xf2eec98c, "Heltec_ct62_companion_radio_usb" }, @@ -101,6 +102,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x1b4399de, "Heltec_t114_repeater_bridge_rs232" }, { 0xf7e47568, "Heltec_t114_repeater_lora_ota_no_external_sensors" }, { 0xb1153422, "Heltec_t114_room_server" }, + { 0x388f2804, "Heltec_t114_sensor" }, { 0x075ca3f9, "Heltec_t114_without_display_companion_radio_ble" }, { 0x2f9f8e7d, "Heltec_t114_without_display_companion_radio_usb" }, { 0xc9dc5d8c, "Heltec_t114_without_display_repeater" }, @@ -189,6 +191,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xe2a1907b, "heltec_v4_expansionkit_tft_companion_radio_ble_ps" }, { 0xbb0ca8db, "heltec_v4_expansionkit_tft_companion_radio_full_femon" }, { 0xf75feb3e, "heltec_v4_kiss_modem" }, + { 0xb0a1e71f, "heltec_v4_partition_expander" }, { 0xbc9cdc70, "heltec_v4_r8_companion_radio_ble" }, { 0x19e31fc6, "heltec_v4_r8_companion_radio_ble_ps" }, { 0xa01b330e, "heltec_v4_r8_companion_radio_full" }, @@ -216,6 +219,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x0dacb683, "heltec_v4_r8_tft_terminal_chat" }, { 0xe792a051, "heltec_v4_repeater" }, { 0x2d5ea842, "heltec_v4_repeater_bridge_espnow" }, + { 0x30c59b1d, "heltec_v4_repeater_legacy_partition_test" }, { 0xab74db9e, "heltec_v4_repeater_observer_mqtt" }, { 0xeed78ce4, "heltec_v4_room_server" }, { 0x7778b7e1, "heltec_v4_room_server_observer_mqtt" }, @@ -395,6 +399,15 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x2d589527, "meshnology_w12_terminal_chat" }, { 0x77d0810c, "Minewsemi_me25ls01_repeater" }, { 0x11b131e7, "Minewsemi_me25ls01_repeater_lora_ota_no_external_sensors" }, + { 0x54850763, "MKE_s3_companion_radio_ble" }, + { 0x0fa61805, "MKE_s3_companion_radio_usb" }, + { 0x8039574f, "MKE_s3_companion_radio_wifi" }, + { 0xa65555d6, "MKE_s3_repeater" }, + { 0x22b1804c, "MKE_s3_repeater_bridge_espnow" }, + { 0xa9f631b1, "MKE_s3_repeater_bridge_rs232" }, + { 0x49d11eca, "MKE_s3_room_server" }, + { 0xd41f0638, "MKE_s3_sensor" }, + { 0xe3105eb8, "MKE_s3_terminal_chat" }, { 0xf8259880, "Nano_G2_Ultra_repeater" }, { 0x063f1f00, "nibble_screen_connect_companion_radio_ble_" }, { 0x70d71f92, "nibble_screen_connect_companion_radio_full_" }, @@ -445,6 +458,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x6925aaf0, "RAK_3112_terminal_chat" }, { 0x2fa509c1, "RAK_3401_repeater_lora_ota_no_external_sensors" }, { 0xa52ba3c8, "RAK_3401_repeater_rak13302_w25q16_lora_ota" }, + { 0xaf0b81dd, "RAK_3401_repeater_unified_lora_ota" }, { 0x9f4d58ac, "RAK_4631_companion_radio_ble" }, { 0x7a55dec3, "RAK_4631_companion_radio_ethernet" }, { 0x5e69738a, "RAK_4631_companion_radio_full" }, @@ -454,6 +468,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x41e33a97, "RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors" }, { 0x29a0da19, "RAK_4631_repeater_lora_ota_no_external_sensors" }, { 0x29bc38dc, "RAK_4631_repeater_rak15001_slot_c_lora_ota" }, + { 0x05f5ffae, "RAK_4631_repeater_unified_lora_ota" }, { 0xc62da371, "RAK_4631_repeater_w25q16_lora_ota" }, { 0xf1d3c5a8, "RAK_4631_terminal_chat" }, { 0xc6d55752, "RAK_WisMesh_Tag_companion_radio_ble" }, @@ -490,6 +505,10 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x2a2f303b, "Station_G3_ESP32_companion_radio_wifi" }, { 0x0ba4453d, "Station_G3_ESP32_kiss_modem" }, { 0xf8d1958f, "Station_G3_ESP32_logging_repeater" }, + { 0x75e85141, "Station_G3_ESP32_r2_repeater" }, + { 0xd62ed92e, "Station_G3_ESP32_r2_repeater_observer_mqtt" }, + { 0x7abaf0d5, "Station_G3_ESP32_r2_room_server" }, + { 0x87edc08b, "Station_G3_ESP32_r2_room_server_observer_mqtt" }, { 0x3c49caf8, "Station_G3_ESP32_repeater" }, { 0x58689ef9, "Station_G3_ESP32_repeater_observer_mqtt" }, { 0x91878dd8, "Station_G3_ESP32_room_server" }, @@ -500,6 +519,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x000fc519, "t1000e_repeater" }, { 0x7b071fa0, "t1000e_repeater_lora_ota_no_external_sensors" }, { 0x3e55e34d, "t1000e_room_server" }, + { 0x54606a4f, "t1000e_sensor" }, { 0x16482630, "T_Beam_S3_Supreme_SX1262_companion_radio_ble" }, { 0xce0f07ad, "T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps" }, { 0x01c43d49, "T_Beam_S3_Supreme_SX1262_companion_radio_wifi" }, @@ -607,6 +627,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xe90a6e74, "Xiao_nrf52_companion_radio_usb" }, { 0x833e5cdc, "Xiao_nrf52_kiss_modem" }, { 0x93145fbb, "Xiao_nrf52_repeater" }, + { 0x8d3dd6d5, "Xiao_nrf52_repeater_bridge_rs232" }, { 0x33a154ff, "Xiao_nrf52_room_server" }, { 0x6658b418, "Xiao_S3_companion_radio_ble" }, { 0x0cfb9c00, "Xiao_S3_companion_radio_ble_ps" }, @@ -623,6 +644,7 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0x396fecc7, "Xiao_S3_WIO_companion_radio_usb" }, { 0xf94e3407, "Xiao_S3_WIO_companion_radio_wifi" }, { 0xe1930103, "Xiao_S3_WIO_kiss_modem" }, + { 0xa4740508, "Xiao_S3_WIO_partition_expander" }, { 0x9f19bcd1, "Xiao_S3_WIO_repeater" }, { 0xfcebff61, "Xiao_S3_WIO_repeater_bridge_espnow" }, { 0xac5c1405, "Xiao_S3_WIO_repeater_observer_mqtt" }, diff --git a/test/test_firmware_capabilities.py b/test/test_firmware_capabilities.py index 5d03bb3a..03c77a54 100644 --- a/test/test_firmware_capabilities.py +++ b/test/test_firmware_capabilities.py @@ -221,6 +221,7 @@ class FirmwareCapabilityCheckerTest(unittest.TestCase): for flags, storage, types in [(16, "internal_flash_and_retained_ram", ["in_place_delta"]), (0, "internal_flash", ["in_place_delta"]), (4, "external_qspi", ["full", "in_place_delta"]), + (48, "adaptive_internal_or_external_qspi", ["full", "in_place_delta"]), (1, "external_sd", ["full", "in_place_delta"])]: result, manifest = self.run_checker( b"dfu invalid in-place patch geometry", "--platform", "NRF52_PLATFORM", diff --git a/test/test_firmware_picker.js b/test/test_firmware_picker.js index 070bbf06..4557b4b5 100644 --- a/test/test_firmware_picker.js +++ b/test/test_firmware_picker.js @@ -895,6 +895,33 @@ assert.strictEqual( picker.humanizeVariant("rak13302-w25q16-lora-ota"), "RAK13302 + External storage board (W25Q16) LoRa OTA" ); +const rakStorageTargets = [ + "RAK_4631_repeater_unified_lora_ota", + "RAK_4631_repeater_lora_ota_no_external_sensors", + "RAK_4631_repeater_rak15001_slot_c_lora_ota", + "RAK_4631_repeater_w25q16_lora_ota", + "RAK_3401_repeater_unified_lora_ota", + "RAK_3401_repeater_lora_ota_no_external_sensors", + "RAK_3401_repeater_rak13302_w25q16_lora_ota", +]; +const rakStorageAssets = rakStorageTargets.map(function (target) { + return asset(target + "-ota-" + family + ".uf2"); +}); +const rakUnifiedCatalog = picker.buildCatalog([ + release(family, "2026-09-25T00:00:00Z", rakStorageAssets), +]); +assert.deepStrictEqual( + rakUnifiedCatalog.profiles.map(function (item) { return item.target; }).sort(), + ["RAK_3401_repeater_unified_lora_ota", "RAK_4631_repeater_unified_lora_ota"].sort() +); +assert(rakUnifiedCatalog.profiles.every(function (item) { + return item.variant === "default" && item.ota === "lora-receiver"; +})); +assert.strictEqual(rakUnifiedCatalog.rows.length, rakStorageTargets.length); +const rakLegacyCatalog = picker.buildCatalog([ + release(family, "2026-09-25T00:00:00Z", rakStorageAssets.slice(1, 4).concat(rakStorageAssets.slice(5))), +]); +assert.strictEqual(rakLegacyCatalog.profiles.length, 5); assert.strictEqual(picker.formatBytes(2097152), "2.00 MiB"); assert.strictEqual( picker.parseFirmwareAsset( diff --git a/test/test_rak_storage_policy.py b/test/test_rak_storage_policy.py new file mode 100644 index 00000000..15f54bed --- /dev/null +++ b/test/test_rak_storage_policy.py @@ -0,0 +1,64 @@ +"""Host regression for the RAK internal/QSPI store selection boundary.""" + +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +class RakStoragePolicyTest(unittest.TestCase): + def test_exact_bootloader_and_nor_pairing(self): + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler unavailable") + source = r''' +#include "src/helpers/ota/OtaRakStoragePolicy.h" +#include +using mesh::ota::RakStorageChoice; +using mesh::ota::rak_storage_choice; +int main() { + const auto I = RakStorageChoice::Internal; + const auto Q = RakStorageChoice::Qspi; + const auto U = RakStorageChoice::Unsafe; + for (bool rak3401 : {false, true}) { + assert(rak_storage_choice(0, false, false, nullptr, rak3401) == I); + assert(rak_storage_choice(0, true, false, nullptr, rak3401) == U); + assert(rak_storage_choice(0, true, true, + rak3401 ? "3401_AUTO_DFU" : "4631_AUTO_DFU", rak3401) == I); + assert(rak_storage_choice(2, false, false, nullptr, rak3401) == I); + assert(rak_storage_choice(3, false, false, nullptr, rak3401) == U); + assert(rak_storage_choice(3, true, true, "3401_W25Q16_DFU", rak3401) == U); + assert(rak_storage_choice(4, true, true, "3401_W25Q16_DFU", rak3401) == U); + assert(rak_storage_choice(2, true, false, nullptr, rak3401) == U); + assert(rak_storage_choice(2, true, true, "4631_15001C_DFU", rak3401) == U); + } + assert(rak_storage_choice(1, false, false, nullptr, false) == I); + assert(rak_storage_choice(1, true, true, "4631_15001C_DFU", false) == Q); + assert(rak_storage_choice(2, true, true, "4631_W25Q16_DFU", false) == Q); + assert(rak_storage_choice(1, true, true, "4631_AUTO_DFU", false) == Q); + assert(rak_storage_choice(2, true, true, "4631_AUTO_DFU", false) == Q); + assert(rak_storage_choice(2, true, true, "3401_AUTO_DFU", false) == U); + assert(rak_storage_choice(1, true, true, "4631_W25Q16_DFU", false) == U); + assert(rak_storage_choice(1, true, true, "4631_15001C_DFU", true) == U); + assert(rak_storage_choice(2, true, true, "3401_W25Q16_DFU", true) == Q); + assert(rak_storage_choice(2, true, true, "3401_AUTO_DFU", true) == Q); + assert(rak_storage_choice(1, true, true, "3401_AUTO_DFU", true) == U); + assert(rak_storage_choice(2, true, true, "4631_W25Q16_DFU", true) == U); +} +''' + source = '#include \n' + source + with tempfile.TemporaryDirectory() as directory: + src = Path(directory) / "policy.cpp" + exe = Path(directory) / "policy" + src.write_text(source) + subprocess.run([compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", + "-I", str(ROOT), str(src), "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/mota/motalib.py b/tools/mota/motalib.py index e005e0e3..a80370f4 100644 --- a/tools/mota/motalib.py +++ b/tools/mota/motalib.py @@ -86,6 +86,7 @@ NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS = 0x02 NRF52_LAYOUT_FLAG_QSPI = 0x04 NRF52_LAYOUT_FLAG_BOOTLOADER_SCRATCH = 0x08 NRF52_LAYOUT_FLAG_HYBRID_RAM = 0x10 +NRF52_LAYOUT_FLAG_AUTO_STORE = 0x20 XIAO_BOOT_BOARD_ID_BASE = 0x28860044 XIAO_BOOT_BOARD_ID_SENSE = 0x28860045 @@ -309,6 +310,10 @@ class Nrf52Layout: def hybrid_ram(self) -> bool: return bool(self.flags & NRF52_LAYOUT_FLAG_HYBRID_RAM) + @property + def auto_store(self) -> bool: + return bool(self.flags & NRF52_LAYOUT_FLAG_AUTO_STORE) + def nrf52_stage_ceiling_for_layout(linked_app_end: int, uses_internal_extrafs: bool) -> int: """Select a safe staging ceiling from linker geometry and actual secondary-storage type.""" if uses_internal_extrafs: @@ -357,11 +362,14 @@ def build_nrf52_layout(layout: Nrf52Layout) -> bytes: raise ValueError("invalid nRF52 app region") known_flags = (NRF52_LAYOUT_FLAG_SD | NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS | NRF52_LAYOUT_FLAG_QSPI | NRF52_LAYOUT_FLAG_BOOTLOADER_SCRATCH | - NRF52_LAYOUT_FLAG_HYBRID_RAM) + NRF52_LAYOUT_FLAG_HYBRID_RAM | NRF52_LAYOUT_FLAG_AUTO_STORE) if layout.flags & ~known_flags: raise ValueError(f"unsupported nRF52 layout flags 0x{layout.flags:X}") if layout.sd_backed and layout.qspi_backed: raise ValueError("nRF52 layout cannot use both SD and QSPI staging") + if layout.auto_store and (layout.sd_backed or layout.qspi_backed or + layout.flags & NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS): + raise ValueError("nRF52 adaptive staging has its own exclusive layout flag") if layout.external_backed and layout.flags & NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS: raise ValueError("nRF52 external staging cannot also reserve internal ExtraFS") if layout.hybrid_ram: diff --git a/tools/mota/nrf52_internal_bootloader_targets.txt b/tools/mota/nrf52_internal_bootloader_targets.txt index 62c1c9bc..0fc55448 100644 --- a/tools/mota/nrf52_internal_bootloader_targets.txt +++ b/tools/mota/nrf52_internal_bootloader_targets.txt @@ -19,6 +19,8 @@ t1000e_repeater_lora_ota_no_external_sensors ThinkNode_M3_repeater_lora_ota_no_external_sensors RAK_3401_repeater_lora_ota_no_external_sensors RAK_4631_repeater_lora_ota_no_external_sensors +RAK_4631_repeater_unified_lora_ota +RAK_3401_repeater_unified_lora_ota RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors GAT562_Mesh_Tracker_Pro_repeater_lora_ota_no_external_sensors diff --git a/tools/mota/pio_endf.py b/tools/mota/pio_endf.py index 5a3b9010..fa9b773c 100644 --- a/tools/mota/pio_endf.py +++ b/tools/mota/pio_endf.py @@ -265,6 +265,7 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f and _builds_companion_radio()) sd_backed = _cppdef("OTA_SD_STORE") is not None qspi_backed = _cppdef("OTA_QSPI_STORE") is not None + auto_store = _cppdef("OTA_RAK_AUTO_STORE") is not None qspi_bootloader_update = _cppdef("OTA_QSPI_BOOTLOADER_UPDATE") is not None hybrid_ram = _cppdef("OTA_HYBRID_RAM_STORE") is not None internal_bootloader_update = _cppdef("OTA_INTERNAL_BOOTLOADER_UPDATE") is not None @@ -276,6 +277,9 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f env["PROJECT_DIR"]) if sd_backed and qspi_backed: raise RuntimeError("nRF52 build cannot enable both SD and QSPI OTA stores") + if auto_store and (not qspi_backed or _cppdef("OTA_FLASH_STORE") is None or + not hybrid_ram or not internal_bootloader_update): + raise RuntimeError("RAK adaptive OTA requires both stores and retained-RAM internal support") if qspi_backed and _cppdef("QSPIFLASH") is not None: raise RuntimeError("raw QSPI OTA staging cannot share a chip with QSPIFLASH") if qspi_bootloader_update: @@ -285,7 +289,7 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f if linked_app_end != ml.NRF52_BOOT_SCRATCH_START: raise RuntimeError("bootloader-update build must link exactly below scratch at 0xE0000") if internal_bootloader_update: - if sd_backed or qspi_backed or _cppdef("QSPIFLASH") is not None or internal_extrafs: + if sd_backed or (qspi_backed and not auto_store) or _cppdef("QSPIFLASH") is not None or internal_extrafs: raise RuntimeError("internal bootloader update cannot use SD/QSPI/ExtraFS") if linked_app_end != ml.NRF52_APP_END: raise RuntimeError("shared-slot bootloader update requires the normal 0xED000 linker ceiling") @@ -297,10 +301,11 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f stage_ceiling = (ml.NRF52_APP_END if (sd_backed or qspi_backed) else ml.nrf52_stage_ceiling_for_layout(linked_app_end, internal_extrafs)) layout_flags = ((ml.NRF52_LAYOUT_FLAG_SD if sd_backed else 0) | - (ml.NRF52_LAYOUT_FLAG_QSPI if qspi_backed else 0) | + (ml.NRF52_LAYOUT_FLAG_QSPI if qspi_backed and not auto_store else 0) | (ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS if internal_extrafs else 0) | (ml.NRF52_LAYOUT_FLAG_BOOTLOADER_SCRATCH if qspi_bootloader_update else 0) | - (ml.NRF52_LAYOUT_FLAG_HYBRID_RAM if hybrid_ram else 0)) + (ml.NRF52_LAYOUT_FLAG_HYBRID_RAM if hybrid_ram else 0) | + (ml.NRF52_LAYOUT_FLAG_AUTO_STORE if auto_store else 0)) layout = ml.Nrf52Layout(app_start, linked_app_end, stage_ceiling, layout_flags) body = ml.ensure_nrf52_layout(raw_body, layout) ident = _firmware_ident() diff --git a/tools/mota/test_mota.py b/tools/mota/test_mota.py index 60c93e24..eb55a826 100644 --- a/tools/mota/test_mota.py +++ b/tools/mota/test_mota.py @@ -52,7 +52,7 @@ def test_internal_bootloader_target_wiring_is_central_and_not_duplicated(): if line.strip() and not line.lstrip().startswith("#") ] assert len(inventory) == len(set(inventory)) - assert len(inventory) == 21 + assert len(inventory) == 23 gat_targets = { "GAT562_30S_Mesh_Kit_repeater_lora_ota_no_external_sensors", "GAT562_Mesh_Tracker_Pro_repeater_lora_ota_no_external_sensors", @@ -66,6 +66,8 @@ def test_internal_bootloader_target_wiring_is_central_and_not_duplicated(): "Heltec_t096_repeater_lora_ota_no_external_sensors", "Heltec_t114_repeater_lora_ota_no_external_sensors", "RAK_3401_repeater_lora_ota_no_external_sensors", + "RAK_3401_repeater_unified_lora_ota", + "RAK_4631_repeater_unified_lora_ota", } <= set(inventory) target_header = (root / "src/helpers/ota/OtaTargets.h").read_text(encoding="utf-8") @@ -1206,6 +1208,14 @@ def test_nrf52_layout_record_roundtrip_and_policy(): ml.ensure_nrf52_layout(_fw(10, 2048), hybrid)) assert ml.parse_nrf52_layout(hybrid_image) == hybrid assert hybrid.hybrid_ram and not hybrid.external_backed + adaptive = ml.Nrf52Layout( + ml.NRF52_APP_BASE_S140_V6, ml.NRF52_APP_END, + ml.NRF52_APP_END, + ml.NRF52_LAYOUT_FLAG_HYBRID_RAM | ml.NRF52_LAYOUT_FLAG_AUTO_STORE) + adaptive_image, _ = ml.ensure_endf( + ml.ensure_nrf52_layout(_fw(11, 2048), adaptive)) + assert ml.parse_nrf52_layout(adaptive_image) == adaptive + assert adaptive.auto_store and adaptive.hybrid_ram and not adaptive.external_backed assert ml.build_nrf52_layout(hybrid) == bytes.fromhex( "6d4f54414c617931011018000060020000d00e0000d00e00") hybrid_v7 = ml.Nrf52Layout( @@ -1228,6 +1238,9 @@ def test_nrf52_layout_record_roundtrip_and_policy(): ml.NRF52_LAYOUT_FLAG_HYBRID_RAM | ml.NRF52_LAYOUT_FLAG_QSPI, ml.NRF52_LAYOUT_FLAG_HYBRID_RAM | ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS, + ml.NRF52_LAYOUT_FLAG_AUTO_STORE | ml.NRF52_LAYOUT_FLAG_QSPI, + ml.NRF52_LAYOUT_FLAG_AUTO_STORE | ml.NRF52_LAYOUT_FLAG_SD, + ml.NRF52_LAYOUT_FLAG_AUTO_STORE | ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS, ): try: ml.build_nrf52_layout(ml.Nrf52Layout( diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 4f534052..826e0944 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -124,6 +124,31 @@ build_src_filter = ${rak3401.build_src_filter} + +<../examples/simple_repeater> +; One full-sensor repeater image for internal flash or the separate-CS W25Q16. +; RAK15001 cannot be selected: it shares the RAK13302 radio chip-select. +[env:RAK_3401_repeater_unified_lora_ota] +extends = rak3401 +extra_scripts = ${nrf52_lora_ota.extra_scripts} + post:variants/rak3401/fix_bsec_lib.py +build_flags = + ${rak3401.build_flags} + ${nrf52_wisblock_auto_ota.build_flags} + -D OTA_QSPI_RAK3401_RADIO_BUS_HANDOFF=1 + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"RAK3401 1W Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D ENABLE_OTA=1 + -D OTA_FOLDER_SERIAL + -D OTA_TARGET_NAME_TABLE=0 + -D OTA_FETCH_PIPELINE=4 +build_src_filter = ${rak3401.build_src_filter} + + + + + +<../examples/simple_repeater> + [env:RAK_3401_room_server] extends = rak3401 build_flags = diff --git a/variants/rak3401/variant.cpp b/variants/rak3401/variant.cpp index a62df4a7..0a51e28b 100644 --- a/variants/rak3401/variant.cpp +++ b/variants/rak3401/variant.cpp @@ -56,8 +56,13 @@ void initVariant() // Keep the separately selected W25Q16 deselected before the RAK13302 starts // using their shared SCK/MOSI/MISO nets. A physical pull-up is still // required so CS# remains defined during reset and core-module swaps. +#if defined(OTA_RAK_AUTO_STORE) + pinMode(31, OUTPUT); + digitalWrite(31, HIGH); +#else pinMode(OTA_QSPI_CS_ARDUINO_PIN, OUTPUT); digitalWrite(OTA_QSPI_CS_ARDUINO_PIN, HIGH); +#endif #endif // Keep the RAK13302 FEM disabled until board startup has completed its diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index df08c29a..7250231d 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -121,6 +121,34 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> +; One full-sensor repeater image for internal flash, W25Q16, or RAK15001 C. +; The matching OTAFIX bootloader for the installed hardware is still required. +[env:RAK_4631_repeater_unified_lora_ota] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + ${nrf52_wisblock_auto_ota.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"RAK4631 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_RS232_BRIDGE=Serial2 + -D WITH_RS232_BRIDGE_UART=2 + -D WITH_RS232_BRIDGE_RX=PIN_SERIAL2_RX + -D WITH_RS232_BRIDGE_TX=PIN_SERIAL2_TX + -D WITH_RS232_BRIDGE_ALT=Serial1 + -D WITH_RS232_BRIDGE_ALT_UART=1 + -D WITH_RS232_BRIDGE_ALT_RX=PIN_SERIAL1_RX + -D WITH_RS232_BRIDGE_ALT_TX=PIN_SERIAL1_TX + -D RS232_BRIDGE_MERGED=1 + -D WITH_RS232_BRIDGE_GPS_CONFLICT_UART=1 +build_src_filter = ${rak4631.build_src_filter} + + + + + +<../examples/simple_repeater> + ; Lean OTA target for space-constrained RAK4631 repeaters. This removes optional external ; environmental sensor packages while retaining the common INA I2C voltage/current monitors and ; RAK12500/RAK12501 GPS support. The legacy no_external_sensors target name remains stable for OTA diff --git a/variants/rak4631/variant.cpp b/variants/rak4631/variant.cpp index d19c8ea3..c339b354 100644 --- a/variants/rak4631/variant.cpp +++ b/variants/rak4631/variant.cpp @@ -46,7 +46,14 @@ void initVariant() pinMode(PIN_LED2, OUTPUT); ledOff(PIN_LED2);; -#if defined(OTA_QSPI_STORE) && defined(OTA_QSPI_CS_ARDUINO_PIN) +#if defined(OTA_RAK_AUTO_STORE) + // Both candidates share SCK/MOSI/MISO. Hold both chip selects high until + // the exact JEDEC probe chooses one; W25Q16 also needs its reset pull-up. + pinMode(26, OUTPUT); + digitalWrite(26, HIGH); + pinMode(31, OUTPUT); + digitalWrite(31, HIGH); +#elif defined(OTA_QSPI_STORE) && defined(OTA_QSPI_CS_ARDUINO_PIN) // Deselect a wired OTA NOR before any other device can toggle shared // WisBlock SPI nets. The W25Q16 installation also needs a physical CS# // pull-up so the signal is defined while the MCU is in reset.