Unify RAK repeater storage firmware and bootloader handoff
This commit is contained in:
1 parent
7a8bddda53
commit
fca10471c3
27 files changed
+723
-29
No files matched your search
+21
-3
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user