Unify RAK repeater storage firmware and bootloader handoff

This commit is contained in:
mikecarper committed 2026-09-25 10:35:04 -07:00
1 parent 7a8bddda53
commit fca10471c3
27 files changed
+723 -29

No files matched your search

+21 -3
View File
@@ -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
+18 -2
View File
@@ -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)
)
)
)
)
+18
View File
@@ -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
+7 -3
View File
@@ -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,
+6 -1
View File
@@ -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
+15 -1
View File
@@ -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();
+38 -3
View File
@@ -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)
+2 -1
View File
@@ -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
+58 -6
View File
@@ -181,6 +181,9 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
"OTA: status | stats | ls | get <id> flash [rescue] | install | rescue install <hash16> | "
"cancel | announce | self | folder | config | key");
#endif
#elif defined(NRF52_PLATFORM) && defined(OTA_RAK_AUTO_STORE)
strcpy(reply,
"OTA: status | stats | ls | get <id> 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));
+32 -2
View File
@@ -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
+1
View File
@@ -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;
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <stdint.h>
#include <string.h>
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
+148
View File
@@ -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 <new>
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<OtaStore*>(&_storage.external) :
_mode == INTERNAL_MODE ? static_cast<OtaStore*>(&_storage.internal) : nullptr;
}
const OtaStore* active() const {
select();
return _mode == QSPI_MODE ? static_cast<const OtaStore*>(&_storage.external) :
_mode == INTERNAL_MODE ? static_cast<const OtaStore*>(&_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
+100
View File
@@ -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) {
+6
View File
@@ -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;
+23 -1
View File
@@ -2,7 +2,7 @@
#include <stdint.h>
// 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" },
+1
View File
@@ -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",
+27
View File
@@ -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(
+64
View File
@@ -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 <assert.h>
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 <initializer_list>\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()
+9 -1
View File
@@ -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:
@@ -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
+8 -3
View File
@@ -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()
+14 -1
View File
@@ -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(
+25
View File
@@ -124,6 +124,31 @@ build_src_filter = ${rak3401.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../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}
+<helpers/ota/*.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
[env:RAK_3401_room_server]
extends = rak3401
build_flags =
+5
View File
@@ -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
+28
View File
@@ -121,6 +121,34 @@ build_src_filter = ${rak4631.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
; One full-sensor repeater image for internal flash, W25Q16, or RAK15001 C.
; The matching OTAFIX bootloader for the installed hardware is still required.
[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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/bridges/RS232Bridge.cpp>
+<../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
+8 -1
View File
@@ -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.