Add opt-in RAK15002 SD firmware seeder with isolated SPI and cache regression tests
This commit is contained in:
1 parent
41775ed707
commit
eebba0cb9a
21 files changed
+1082
-16
No files matched your search
@@ -3632,6 +3632,9 @@ bool CommonCLI::handleSdCardSetCmd(const char* config, char* reply) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Heap-backed source-only builds may not have an OTA workspace yet. Never
|
||||
// dereference it (or start card maintenance) after an allocation failure.
|
||||
if (!mesh::ota::ota_acquire_context(reply, 160)) return true;
|
||||
mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
|
||||
if (context.apply_pending) {
|
||||
strcpy(reply, "Error: OTA apply is pending");
|
||||
@@ -3730,6 +3733,7 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
|
||||
_sdcard_erase_at, now);
|
||||
snprintf(reply, 160, "> erase: %s", age);
|
||||
} else if (strcmp(query, "free") == 0) {
|
||||
if (!mesh::ota::ota_acquire_context(reply, 160)) return true;
|
||||
uint64_t used_bytes = 0;
|
||||
uint64_t free_bytes = 0;
|
||||
mesh::ota::OtaStoreSdNrf52& store = mesh::ota::ota_ctx().sdStagingStore();
|
||||
@@ -3761,6 +3765,7 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
|
||||
}
|
||||
page = parsed;
|
||||
}
|
||||
if (!mesh::ota::ota_acquire_context(reply, 160)) return true;
|
||||
mesh::ota::OtaStoreSdNrf52& store = mesh::ota::ota_ctx().sdStagingStore();
|
||||
if (!store.listFiles(*_board, (uint16_t)page, reply, 160)) {
|
||||
snprintf(reply, 160, "Error: SD card list failed: %s", store.last_error());
|
||||
|
||||
@@ -219,6 +219,10 @@ bool OtaCacheSdNrf52::begin(uint32_t total_size) {
|
||||
pathFor(_mid, "mota", final_path, sizeof(final_path));
|
||||
pathFor(_mid, "part", part_path, sizeof(part_path));
|
||||
if (contains(_mid)) { fail("SD archive already contains this mOTA"); return false; }
|
||||
// The index is also the advertised archive capacity. Do not spend LoRa
|
||||
// airtime/card space on a new container that this build cannot seed. Check
|
||||
// before quarantining or replacing any existing final/partial bytes.
|
||||
if (_count >= OTA_SD_CACHE_MAX) { fail("SD archive entry limit reached"); return false; }
|
||||
if (_owner->_sd->exists(final_path) && !quarantineFinal(_mid)) return false;
|
||||
// begin() means resumeStaged() already rejected any old partial. Drop it before measuring space so a
|
||||
// corrupt/obsolete .part is not double-counted against the new allocation and the install reserve.
|
||||
|
||||
@@ -407,7 +407,11 @@ static __attribute__((noinline)) bool handle_control_command(
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
snprintf(reply, 160,
|
||||
"OTA seeder: status | stats | ls=find images | get <id> folder=capture | cancel | "
|
||||
"announce | folder | config. LoRa install is disabled.");
|
||||
"announce | folder | "
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
"cache | "
|
||||
#endif
|
||||
"config. LoRa install is disabled.");
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE)
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
@@ -817,7 +821,7 @@ static __attribute__((noinline)) bool handle_control_command(
|
||||
} else if (is_cmd(a, "announce|adv", &rest)) {
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
c.manager.announce();
|
||||
snprintf(reply, 160, "OK beacon sent (serving=%u host mOTA)",
|
||||
snprintf(reply, 160, "OK beacon sent (serving=%u external mOTA)",
|
||||
(unsigned)c.manager.servedCount());
|
||||
#else
|
||||
if (c.self_serve_supported && !c.serving) c.serving = ota_serve_self(c, 0);
|
||||
@@ -875,7 +879,10 @@ static __attribute__((noinline)) bool handle_control_command(
|
||||
#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_TOWER_AUTO_STORE)
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
(void)bl;
|
||||
snprintf(reply + n, 160 - n, " | source-only; local install disabled");
|
||||
#elif defined(OTA_TOWER_AUTO_STORE)
|
||||
snprintf(reply + n, 160 - n, " | store:%s | bootloader:%s",
|
||||
c.fetch_store.usesExternal() ? "SD" : c.fetch_store.usesInternal() ? "internal" : "unsafe",
|
||||
c.fetch_store.compatible() ? "apply OK" : "unsupported");
|
||||
|
||||
@@ -114,6 +114,12 @@ 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;
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
// An archive is not an installation backend. Source-only boards retain a
|
||||
// separate SD owner for the shared FAT volume without granting SD apply.
|
||||
OtaStoreSdNrf52 sd_archive_store;
|
||||
OtaCacheSdNrf52 sd_cache;
|
||||
#endif
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_TOWER_AUTO_STORE)
|
||||
OtaStoreTowerNrf52 fetch_store; // SD primary, qualified internal delta fallback
|
||||
OtaCacheSdNrf52 sd_cache; // always owns the stable SD store
|
||||
@@ -681,7 +687,9 @@ struct OtaContext {
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
OtaStoreSdNrf52& sdStagingStore() {
|
||||
#if defined(OTA_TOWER_AUTO_STORE)
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
return sd_archive_store;
|
||||
#elif defined(OTA_TOWER_AUTO_STORE)
|
||||
return fetch_store.sdStore();
|
||||
#else
|
||||
return fetch_store;
|
||||
@@ -819,7 +827,11 @@ struct OtaContext {
|
||||
if (!sd_cache.autoCaptureEnabled() || apply_pending || manager.fetchState() != OtaManager::IDLE ||
|
||||
(_sd_cache_next_at && (int32_t)(now - _sd_cache_next_at) < 0)) return;
|
||||
|
||||
for (uint8_t i = 0; i < manager.catalogCount(); i++) {
|
||||
// The index is also the publication limit: do not download containers
|
||||
// that cannot be served. contains() can discover a valid file and grow
|
||||
// the index, so recheck capacity before considering each catalog row.
|
||||
for (uint8_t i = 0; i < manager.catalogCount() &&
|
||||
sd_cache.capturedCount() < OTA_SD_CACHE_MAX; i++) {
|
||||
const OtaManager::CatRow* row = manager.catalogRow(i);
|
||||
if (!row || row->have_max == 0 || sd_cache.contains(row->mid)) continue;
|
||||
if (!manager.catalogReady(row, now)) continue;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <SPI.h>
|
||||
|
||||
#if defined(OTA_RAK15002_SD_CACHE)
|
||||
#if !defined(NRF52_PLATFORM) || !defined(RAK_4631) || !defined(OTA_SD_STORE) || !defined(OTA_SEEDER_ONLY)
|
||||
#error "RAK15002 SD cache requires the exact nRF52 RAK4631 seeder-only profile"
|
||||
#endif
|
||||
#if defined(ETHERNET_ENABLED) || defined(OTA_QSPI_STORE) || defined(OTA_RAK_AUTO_STORE) || defined(OTA_TOWER_AUTO_STORE)
|
||||
#error "RAK15002 SD cache cannot share the WisBlock SPI/CS with Ethernet or OTA NOR"
|
||||
#endif
|
||||
#if SPI_INTERFACES_COUNT != 1 || (defined(SPI_32MHZ_INTERFACE) && SPI_32MHZ_INTERFACE != 0)
|
||||
#error "RAK15002 SD cache requires radio SPI on SPIM3 and reserves SPIM2 for the SD card"
|
||||
#endif
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
#error "RAK15002 cache profile has no qualified bootloader-update handoff"
|
||||
#endif
|
||||
#ifndef OTA_SD_CS_PIN
|
||||
#define OTA_SD_CS_PIN 26
|
||||
#endif
|
||||
static_assert(OTA_SD_CS_PIN == 26 && PIN_SPI_MISO == 29 && PIN_SPI_SCK == 3 && PIN_SPI_MOSI == 30,
|
||||
"RAK15002 exact WisBlock SPI pin map changed; requalify the profile");
|
||||
#elif !defined(OTA_SD_CS_PIN)
|
||||
#define OTA_SD_CS_PIN PIN_SPI1_NSS
|
||||
#endif
|
||||
|
||||
namespace mesh { namespace ota {
|
||||
|
||||
inline SPIClass& ota_sd_spi_bus() {
|
||||
#if defined(OTA_RAK15002_SD_CACHE)
|
||||
// The RAK radio reassigns global SPI/SPIM3 to its private pins. Never use
|
||||
// that object for the external WisBlock bus, or reconfigure it on SD mount.
|
||||
static SPIClass sd_spi(NRF_SPIM2, 29, 3, 30);
|
||||
return sd_spi;
|
||||
#else
|
||||
return SPI1; // unchanged MeshTower SD bus
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void ota_sd_prepare_chip_select() {
|
||||
digitalWrite(OTA_SD_CS_PIN, HIGH); // preload latch before enabling output
|
||||
pinMode(OTA_SD_CS_PIN, OUTPUT);
|
||||
}
|
||||
|
||||
} } // namespace mesh::ota
|
||||
@@ -15,11 +15,9 @@
|
||||
#include "OtaSdAuthRecord.h"
|
||||
#include "OtaSdBootToken.h"
|
||||
#include "OtaSelf.h"
|
||||
#include "OtaSdSpiBusNrf52.h"
|
||||
#include "flash/flash_nrf5x.h"
|
||||
|
||||
#ifndef OTA_SD_CS_PIN
|
||||
#define OTA_SD_CS_PIN PIN_SPI1_NSS
|
||||
#endif
|
||||
#ifndef OTA_SD_SCK_MHZ
|
||||
#define OTA_SD_SCK_MHZ 8
|
||||
#endif
|
||||
@@ -90,9 +88,10 @@ void OtaStoreSdNrf52::resetStoreState() {
|
||||
bool OtaStoreSdNrf52::beginCardOnly() {
|
||||
_error[0] = 0;
|
||||
resetStoreState();
|
||||
ota_sd_prepare_chip_select();
|
||||
if (!_sd || !_file ||
|
||||
!_sd->cardBegin(SdSpiConfig(OTA_SD_CS_PIN, DEDICATED_SPI,
|
||||
SD_SCK_MHZ(OTA_SD_SCK_MHZ), &SPI1))) {
|
||||
SD_SCK_MHZ(OTA_SD_SCK_MHZ), &ota_sd_spi_bus()))) {
|
||||
fail("SD card init failed");
|
||||
return false;
|
||||
}
|
||||
@@ -102,8 +101,9 @@ bool OtaStoreSdNrf52::beginCardOnly() {
|
||||
bool OtaStoreSdNrf52::mount() {
|
||||
if (_mounted) return true;
|
||||
_error[0] = 0;
|
||||
ota_sd_prepare_chip_select();
|
||||
if (!_sd || !_file || !_sd->begin(SdSpiConfig(OTA_SD_CS_PIN, DEDICATED_SPI,
|
||||
SD_SCK_MHZ(OTA_SD_SCK_MHZ), &SPI1))) {
|
||||
SD_SCK_MHZ(OTA_SD_SCK_MHZ), &ota_sd_spi_bus()))) {
|
||||
fail("SD mount failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1363,9 +1363,11 @@ uint8_t EnvironmentSensorManager::detectWisBlockStorageHolds() {
|
||||
|| (w25_id != 0 && w25_id != 0xFFFFFFUL)) {
|
||||
holds |= mesh::gps::ExternalStorage;
|
||||
}
|
||||
#elif defined(OTA_QSPI_SHARED_WISBLOCK_SPI) || defined(OTA_SD_STORE)
|
||||
#elif defined(OTA_QSPI_SHARED_WISBLOCK_SPI) || (defined(OTA_SD_STORE) && !defined(OTA_RAK15002_SD_CACHE))
|
||||
holds |= mesh::gps::ExternalStorage;
|
||||
#endif
|
||||
// Exact RAK15002 uses connector pin 5 (unswitched 3V3), not 3V3_S. Its
|
||||
// archive must not accidentally keep a GPS-only sensor rail powered.
|
||||
return holds;
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in new issue
Block a user