725 lines
22 KiB
C++
725 lines
22 KiB
C++
#include "OtaStoreSdNrf52.h"
|
|
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
|
|
#include <Arduino.h>
|
|
#include <SdFat.h>
|
|
#include <SPI.h>
|
|
#include <new>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "MeshCore.h"
|
|
#include "OtaBootloaderUpdate.h"
|
|
#include "OtaByteIO.h"
|
|
#include "OtaFlashLayout_nrf52.h"
|
|
#include "OtaSdAuthRecord.h"
|
|
#include "OtaSdBootToken.h"
|
|
#include "OtaSelf.h"
|
|
#include "OtaSdSpiBusNrf52.h"
|
|
#include "flash/flash_nrf5x.h"
|
|
|
|
#ifndef OTA_SD_SCK_MHZ
|
|
#define OTA_SD_SCK_MHZ 8
|
|
#endif
|
|
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
// The mounted FAT/exFAT volume and both retained file handles (staging and
|
|
// archive) are lazy heap objects, not linked globals. Bind their total plus
|
|
// allocator metadata to the runtime RAM policy's separate 2 KiB allowance.
|
|
static_assert(sizeof(mesh::ota::OtaSdFs) + 2u * sizeof(mesh::ota::OtaSdFile) + 3u * 32u <= 2048u,
|
|
"RAK15002 FAT volume/file allocations exceed the RAM allowance");
|
|
#endif
|
|
|
|
namespace mesh {
|
|
namespace ota {
|
|
|
|
namespace {
|
|
|
|
static void clear_sd_auth_record() {
|
|
volatile uint8_t* const retained =
|
|
reinterpret_cast<volatile uint8_t*>(MOTA_SD_AUTH_ADDR);
|
|
for (uint32_t i = 0; i < MOTA_SD_AUTH_LEN; ++i) retained[i] = 0;
|
|
__DMB();
|
|
__DSB();
|
|
}
|
|
|
|
#if !defined(OTA_SEEDER_ONLY)
|
|
static bool publish_sd_auth_record(const uint8_t record[MOTA_SD_AUTH_LEN]) {
|
|
volatile uint8_t* const retained =
|
|
reinterpret_cast<volatile uint8_t*>(MOTA_SD_AUTH_ADDR);
|
|
// Invalidate magic first, then publish the body and magic last. A reset at any
|
|
// intermediate instruction therefore leaves a record OTAFIX rejects.
|
|
for (uint32_t i = 0; i < sizeof(MOTA_SD_AUTH_MAGIC); ++i) retained[i] = 0;
|
|
__DMB();
|
|
for (uint32_t i = sizeof(MOTA_SD_AUTH_MAGIC); i < MOTA_SD_AUTH_LEN; ++i)
|
|
retained[i] = record[i];
|
|
__DMB();
|
|
for (uint32_t i = 0; i < sizeof(MOTA_SD_AUTH_MAGIC); ++i) retained[i] = record[i];
|
|
__DMB();
|
|
__DSB();
|
|
for (uint32_t i = 0; i < MOTA_SD_AUTH_LEN; ++i)
|
|
if (retained[i] != record[i]) return false;
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
} // namespace
|
|
|
|
const char* const OtaStoreSdNrf52::PATH = "/meshcore-ota.mota";
|
|
|
|
OtaStoreSdNrf52::OtaStoreSdNrf52()
|
|
#if !defined(OTA_RAK15002_SD_STORE)
|
|
: _sd(new (std::nothrow) OtaSdFs()), _file(new (std::nothrow) OtaSdFile())
|
|
#endif
|
|
{
|
|
// If an earlier reset did not enter OTAFIX, stale authorization must not arm a
|
|
// later unrelated reboot. OTAFIX runs before application constructors and
|
|
// consumes a valid record on the intended apply boot.
|
|
clear_sd_auth_record();
|
|
}
|
|
|
|
OtaStoreSdNrf52::~OtaStoreSdNrf52() {
|
|
resetStoreState();
|
|
if (_file) delete _file;
|
|
if (_sd) delete _sd;
|
|
releaseBusClaim();
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::acquireBus() {
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
if (!ota_rak15002_sd_card_present()) { fail("SD card absent"); return false; }
|
|
if (!rak4631_try_acquire_spi(RakSharedSpiOwner::Sd)) {
|
|
fail("WisBlock SPI owned by Ethernet or NOR"); return false;
|
|
}
|
|
_bus_owned = true;
|
|
if (!_sd) _sd = new (std::nothrow) OtaSdFs();
|
|
if (!_file) _file = new (std::nothrow) OtaSdFile();
|
|
if (!_sd || !_file) {
|
|
fail("SD allocation failed"); releaseBusClaim(); return false;
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::busAccessAllowed() const {
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
return _bus_owned && rak4631_spi_owner() == RakSharedSpiOwner::Sd
|
|
&& ota_rak15002_sd_card_present();
|
|
#else
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
void OtaStoreSdNrf52::releaseBusClaim() {
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
if (!_bus_owned) return;
|
|
if (_bus_started) {
|
|
ota_sd_spi_bus().end();
|
|
// Leave the powered, unswitched card deselected, and stop driving the
|
|
// shared clock/data pads before another peripheral claims them.
|
|
digitalWrite(OTA_SD_CS_PIN, HIGH);
|
|
pinMode(OTA_SD_CS_PIN, OUTPUT);
|
|
pinMode(3, INPUT); pinMode(29, INPUT); pinMode(30, INPUT);
|
|
_bus_started = false;
|
|
}
|
|
rak4631_release_spi(RakSharedSpiOwner::Sd);
|
|
_bus_owned = false;
|
|
#endif
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::releaseBus() {
|
|
if (_total) { fail("SD staged update is pinned"); return false; }
|
|
resetStoreState();
|
|
releaseBusClaim();
|
|
return true;
|
|
}
|
|
|
|
void OtaStoreSdNrf52::fail(const char* message) {
|
|
strncpy(_error, message ? message : "SD error", sizeof(_error) - 1);
|
|
_error[sizeof(_error) - 1] = 0;
|
|
}
|
|
|
|
void OtaStoreSdNrf52::resetStoreState() {
|
|
_total = 0;
|
|
_first_sector = 0;
|
|
_allocated_sectors = 0;
|
|
_planned_bootloader = false;
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
// Ownership persists if a card is physically removed. Cleanup still owns
|
|
// the controller and can release it, but no other device may steal it.
|
|
const bool access = _bus_owned && rak4631_spi_owner() == RakSharedSpiOwner::Sd;
|
|
#else
|
|
const bool access = true;
|
|
#endif
|
|
if (access && _file && *_file) _file->close();
|
|
if (access && _sd) _sd->end();
|
|
_mounted = false;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::beginCardOnly() {
|
|
_error[0] = 0;
|
|
if (!acquireBus()) return false;
|
|
resetStoreState();
|
|
ota_sd_prepare_chip_select();
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
_bus_started = true;
|
|
#endif
|
|
if (!_sd || !_file ||
|
|
!_sd->cardBegin(SdSpiConfig(OTA_SD_CS_PIN, DEDICATED_SPI,
|
|
SD_SCK_MHZ(OTA_SD_SCK_MHZ), &ota_sd_spi_bus()))) {
|
|
fail("SD card init failed");
|
|
resetStoreState(); releaseBusClaim();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::mount() {
|
|
if (_mounted) return busAccessAllowed();
|
|
_error[0] = 0;
|
|
if (!acquireBus()) return false;
|
|
ota_sd_prepare_chip_select();
|
|
#if defined(OTA_RAK15002_SD_STORE)
|
|
_bus_started = true;
|
|
#endif
|
|
if (!_sd || !_file || !_sd->begin(SdSpiConfig(OTA_SD_CS_PIN, DEDICATED_SPI,
|
|
SD_SCK_MHZ(OTA_SD_SCK_MHZ), &ota_sd_spi_bus()))) {
|
|
fail("SD mount failed");
|
|
resetStoreState(); releaseBusClaim();
|
|
return false;
|
|
}
|
|
_mounted = true;
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::formatCard(MainBoard& board) {
|
|
clear_sd_auth_record();
|
|
if (!beginCardOnly()) return false;
|
|
|
|
board.serviceWatchdog();
|
|
if (!_sd->format()) {
|
|
fail("SD format failed");
|
|
_sd->end();
|
|
releaseBusClaim();
|
|
return false;
|
|
}
|
|
board.serviceWatchdog();
|
|
if (!_sd->volumeBegin()) {
|
|
fail("SD remount after format failed");
|
|
_sd->end();
|
|
releaseBusClaim();
|
|
return false;
|
|
}
|
|
|
|
_mounted = true;
|
|
board.serviceWatchdog();
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::eraseCard(MainBoard& board) {
|
|
clear_sd_auth_record();
|
|
if (!beginCardOnly()) return false;
|
|
|
|
SdCard* card = _sd->card();
|
|
const uint32_t sector_count = card ? card->sectorCount() : 0;
|
|
if (!card || sector_count == 0) {
|
|
fail("SD sector count failed");
|
|
_sd->end();
|
|
releaseBusClaim();
|
|
return false;
|
|
}
|
|
|
|
// Match SdFat's formatter example: bounded erase ranges avoid cards that
|
|
// reject or time out on a single whole-device erase command. Servicing the
|
|
// board watchdog between ranges keeps the intentionally long operation from
|
|
// looking like a wedged main loop.
|
|
const uint32_t erase_sectors = 262144UL;
|
|
uint32_t first_sector = 0;
|
|
while (first_sector < sector_count) {
|
|
const uint32_t remaining = sector_count - first_sector;
|
|
const uint32_t count = remaining < erase_sectors ? remaining : erase_sectors;
|
|
const uint32_t last_sector = first_sector + count - 1;
|
|
board.serviceWatchdog();
|
|
if (!card->erase(first_sector, last_sector)) {
|
|
fail("SD erase failed");
|
|
_sd->end();
|
|
releaseBusClaim();
|
|
return false;
|
|
}
|
|
first_sector += count;
|
|
}
|
|
|
|
board.serviceWatchdog();
|
|
if (!card->syncDevice()) {
|
|
fail("SD erase sync failed");
|
|
_sd->end();
|
|
releaseBusClaim();
|
|
return false;
|
|
}
|
|
_sd->end();
|
|
releaseBusClaim();
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::getSpace(MainBoard& board, uint64_t& used_bytes,
|
|
uint64_t& free_bytes) {
|
|
used_bytes = 0;
|
|
free_bytes = 0;
|
|
_error[0] = 0;
|
|
if (!mount()) return false;
|
|
|
|
board.serviceWatchdog();
|
|
const uint32_t cluster_count = _sd->clusterCount();
|
|
const uint32_t free_clusters = _sd->freeClusterCount();
|
|
const uint32_t bytes_per_cluster = _sd->bytesPerCluster();
|
|
board.serviceWatchdog();
|
|
if (cluster_count == 0 || bytes_per_cluster == 0 ||
|
|
free_clusters > cluster_count) {
|
|
fail("SD space query failed");
|
|
return false;
|
|
}
|
|
|
|
free_bytes = (uint64_t)free_clusters * bytes_per_cluster;
|
|
used_bytes = (uint64_t)(cluster_count - free_clusters) * bytes_per_cluster;
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
|
|
static const uint16_t SD_LIST_PAGE_SIZE = 2;
|
|
static const size_t SD_LIST_NAME_CAP = 256; // FAT/exFAT long-name maximum plus NUL
|
|
static const size_t SD_LIST_SHOWN_PATH = 50; // bounded CLI line; long paths get an explicit leading "..."
|
|
|
|
struct SdListState {
|
|
SdListState(MainBoard& b, uint32_t f) : board(b), first(f) {}
|
|
MainBoard& board;
|
|
uint32_t first;
|
|
uint32_t total = 0;
|
|
char body[132] = {0};
|
|
size_t used = 0;
|
|
char* path = nullptr;
|
|
size_t path_len = 0;
|
|
size_t path_cap = 0;
|
|
bool failed = false;
|
|
};
|
|
|
|
static void compact_file_size(char* out, size_t cap, uint64_t bytes) {
|
|
static const char units[] = {'B', 'K', 'M', 'G', 'T'};
|
|
uint8_t unit = 0;
|
|
uint64_t whole = bytes;
|
|
while (whole >= 1024 && unit < sizeof(units) - 1) { whole /= 1024; unit++; }
|
|
snprintf(out, cap, "%lu%c", (unsigned long)whole, units[unit]);
|
|
}
|
|
|
|
static bool ensure_list_path(SdListState& state, size_t need) {
|
|
if (need <= state.path_cap) return true;
|
|
size_t cap = state.path_cap ? state.path_cap : 256;
|
|
while (cap < need) {
|
|
if (cap > SIZE_MAX / 2) { state.failed = true; return false; }
|
|
cap *= 2;
|
|
}
|
|
char* grown = static_cast<char*>(realloc(state.path, cap));
|
|
if (!grown) { state.failed = true; return false; }
|
|
state.path = grown;
|
|
state.path_cap = cap;
|
|
return true;
|
|
}
|
|
|
|
static bool append_sd_list_entry(SdListState& state, const char* path, uint64_t bytes) {
|
|
char size[12];
|
|
compact_file_size(size, sizeof(size), bytes);
|
|
const size_t path_len = strlen(path);
|
|
const char* shown = path;
|
|
char shortened[SD_LIST_SHOWN_PATH + 1];
|
|
if (path_len > SD_LIST_SHOWN_PATH) {
|
|
memcpy(shortened, "...", 3);
|
|
memcpy(shortened + 3, path + path_len - (SD_LIST_SHOWN_PATH - 3), SD_LIST_SHOWN_PATH - 3);
|
|
shortened[SD_LIST_SHOWN_PATH] = 0;
|
|
shown = shortened;
|
|
}
|
|
size_t remain = sizeof(state.body) - state.used;
|
|
int n = snprintf(state.body + state.used, remain, "\n %s %s", shown, size);
|
|
if (n <= 0 || (size_t)n >= remain) { state.failed = true; return false; }
|
|
state.used += (size_t)n;
|
|
return true;
|
|
}
|
|
|
|
static bool scan_sd_files(OtaSdFile& dir, SdListState& state) {
|
|
OtaSdFile file;
|
|
while (file.openNext(&dir, O_RDONLY)) {
|
|
state.board.serviceWatchdog();
|
|
const size_t parent_len = state.path_len;
|
|
if (parent_len > SIZE_MAX - SD_LIST_NAME_CAP - 2 ||
|
|
!ensure_list_path(state, parent_len + SD_LIST_NAME_CAP + 2)) {
|
|
file.close();
|
|
return false;
|
|
}
|
|
size_t name_at = parent_len;
|
|
if (name_at == 0 || state.path[name_at - 1] != '/') state.path[name_at++] = '/';
|
|
state.path[name_at] = 0;
|
|
file.getName(state.path + name_at, SD_LIST_NAME_CAP);
|
|
size_t name_len = strlen(state.path + name_at);
|
|
if (name_len == 0) { state.failed = true; file.close(); return false; }
|
|
state.path_len = name_at + name_len;
|
|
|
|
if (file.isDir()) {
|
|
if (!scan_sd_files(file, state)) {
|
|
state.path_len = parent_len;
|
|
state.path[parent_len] = 0;
|
|
file.close();
|
|
return false;
|
|
}
|
|
} else {
|
|
uint32_t index = state.total++;
|
|
if (index >= state.first && index < state.first + SD_LIST_PAGE_SIZE) {
|
|
if (!append_sd_list_entry(state, state.path, file.fileSize())) {
|
|
state.path_len = parent_len;
|
|
state.path[parent_len] = 0;
|
|
file.close();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
state.path_len = parent_len;
|
|
state.path[parent_len] = 0;
|
|
file.close();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool OtaStoreSdNrf52::listFiles(MainBoard& board, uint16_t page,
|
|
char* reply, size_t cap) {
|
|
if (!reply || cap == 0) return false;
|
|
if (page == 0) page = 1;
|
|
_error[0] = 0;
|
|
if (!mount()) return false;
|
|
|
|
OtaSdFile root = _sd->open("/", O_RDONLY);
|
|
if (!root || !root.isDir()) { fail("SD root directory open failed"); return false; }
|
|
SdListState state(board, (uint32_t)(page - 1) * SD_LIST_PAGE_SIZE);
|
|
bool scanned = ensure_list_path(state, 2) && scan_sd_files(root, state);
|
|
root.close();
|
|
free(state.path);
|
|
if (!scanned || state.failed) { fail("SD file listing failed"); return false; }
|
|
|
|
uint32_t pages = (state.total + SD_LIST_PAGE_SIZE - 1) / SD_LIST_PAGE_SIZE;
|
|
if (pages == 0) {
|
|
snprintf(reply, cap, "> SD files: empty");
|
|
} else if (page > pages) {
|
|
snprintf(reply, cap, "Error: SD file page %u out of range (1-%lu)",
|
|
(unsigned)page, (unsigned long)pages);
|
|
} else {
|
|
snprintf(reply, cap, "> SD files %u/%lu (%lu):%s",
|
|
(unsigned)page, (unsigned long)pages,
|
|
(unsigned long)state.total, state.body);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
uint32_t OtaStoreSdNrf52::capacity() const {
|
|
if (!busAccessAllowed() || !_mounted || !_sd || !_sd->card()) return 0;
|
|
uint64_t bytes = (uint64_t)_sd->card()->sectorCount() * MOTA_SD_AUTH_SECTOR_SIZE;
|
|
return bytes > UINT32_MAX ? UINT32_MAX : (uint32_t)bytes;
|
|
}
|
|
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
// The source-only context uses OtaStoreRam<1> for install fetches and this
|
|
// object only owns the FAT volume shared with OtaCacheSdNrf52. Its archive
|
|
// has its own file/read/write methods. Do not retain unreachable installer
|
|
// vtable code (or permit accidental staging/approval) in a Companion image.
|
|
bool OtaStoreSdNrf52::plan_layout(bool, uint32_t, uint32_t, uint32_t, bool) {
|
|
fail("source-only SD archive cannot stage/apply"); return false;
|
|
}
|
|
bool OtaStoreSdNrf52::begin(uint32_t) {
|
|
fail("source-only SD archive cannot stage/apply"); return false;
|
|
}
|
|
bool OtaStoreSdNrf52::set_meta_size(uint32_t) { return false; }
|
|
bool OtaStoreSdNrf52::write(uint32_t, const uint8_t*, uint32_t) { return false; }
|
|
bool OtaStoreSdNrf52::read(uint32_t, uint8_t*, uint32_t) const { return false; }
|
|
bool OtaStoreSdNrf52::finalize() { return false; }
|
|
void OtaStoreSdNrf52::checkpoint() {}
|
|
bool OtaStoreSdNrf52::reopen() { return false; }
|
|
bool OtaStoreSdNrf52::discard() { return false; }
|
|
void OtaStoreSdNrf52::clear() {}
|
|
bool OtaStoreSdNrf52::approve_for_bootloader(const uint8_t[32], const uint8_t[32]) {
|
|
fail("source-only SD archive cannot stage/apply"); return false;
|
|
}
|
|
#else
|
|
bool OtaStoreSdNrf52::locate_file() {
|
|
if (!busAccessAllowed()) return false;
|
|
Sector_t first = 0, last = 0;
|
|
if (!_file || !_file->contiguousRange(&first, &last) || last < first) {
|
|
fail("OTA file is not contiguous");
|
|
return false;
|
|
}
|
|
uint64_t need = ((uint64_t)_total + MOTA_SD_AUTH_SECTOR_SIZE - 1) /
|
|
MOTA_SD_AUTH_SECTOR_SIZE;
|
|
uint64_t available = (uint64_t)last - first + 1;
|
|
const uint32_t card_sectors = _sd->card() ? _sd->card()->sectorCount() : 0;
|
|
if (need == 0 || need > available || first == 0 || card_sectors == 0 ||
|
|
first >= card_sectors || need > card_sectors - first) {
|
|
fail("OTA file sector range is invalid");
|
|
return false;
|
|
}
|
|
_first_sector = (uint32_t)first;
|
|
_allocated_sectors = (uint32_t)need;
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::plan_layout(bool, uint32_t image_size,
|
|
uint32_t, uint32_t payload_size,
|
|
bool is_bootloader) {
|
|
_planned_bootloader = false;
|
|
if (is_bootloader) {
|
|
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
|
if (ota_bootloader_image_geometry_valid(image_size, payload_size)) {
|
|
_planned_bootloader = true;
|
|
return true;
|
|
}
|
|
fail("bootloader package geometry mismatch");
|
|
#else
|
|
fail("bootloader update is not enabled for this SD target");
|
|
#endif
|
|
return false;
|
|
}
|
|
const uint32_t app_base = mota_nrf52_app_base();
|
|
if (image_size == 0 || payload_size == 0 || app_base >= MOTA_NRF52_APP_END ||
|
|
image_size > MOTA_NRF52_APP_END - app_base) {
|
|
fail("image exceeds nRF52 application region");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::begin(uint32_t total_size) {
|
|
_total = 0;
|
|
_first_sector = 0;
|
|
_allocated_sectors = 0;
|
|
clear_sd_auth_record();
|
|
if (total_size < 13 || !mount()) return false;
|
|
if (*_file) _file->close();
|
|
_sd->remove(PATH);
|
|
if (!_file->open(PATH, O_RDWR | O_CREAT | O_EXCL)) {
|
|
fail("SD OTA file create failed");
|
|
return false;
|
|
}
|
|
if (!_file->preAllocate(total_size)) {
|
|
fail("SD lacks contiguous space for update");
|
|
_file->close();
|
|
_sd->remove(PATH);
|
|
return false;
|
|
}
|
|
_total = total_size;
|
|
if (!locate_file()) {
|
|
_total = 0;
|
|
_file->close();
|
|
_sd->remove(PATH);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::set_meta_size(uint32_t meta_bytes) {
|
|
if (!_total || meta_bytes > _total - 5) return false;
|
|
uint8_t erased[512];
|
|
memset(erased, 0xFF, sizeof(erased));
|
|
for (uint32_t off = 0; off < meta_bytes; ) {
|
|
uint32_t n = meta_bytes - off;
|
|
if (n > sizeof(erased)) n = sizeof(erased);
|
|
if (!write(off, erased, n)) return false;
|
|
off += n;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::write(uint32_t offset, const uint8_t* data, uint32_t len) {
|
|
if (!busAccessAllowed() || !_file || !*_file || (uint64_t)offset + len > _total || !_file->seekSet(offset) ||
|
|
_file->write(data, len) != len) {
|
|
fail("SD OTA write failed");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::read(uint32_t offset, uint8_t* buf, uint32_t len) const {
|
|
if (!busAccessAllowed() || !_file || !*_file || (uint64_t)offset + len > _total || !_file->seekSet(offset) ||
|
|
_file->read(buf, len) != (int)len) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::finalize() {
|
|
if (!busAccessAllowed()) return false;
|
|
SdCard* const card = _sd ? _sd->card() : nullptr;
|
|
if (!_file || !*_file || !card || !_file->sync() || !card->syncDevice()) {
|
|
fail("SD OTA sync failed");
|
|
return false;
|
|
}
|
|
return locate_file();
|
|
}
|
|
|
|
void OtaStoreSdNrf52::checkpoint() {
|
|
if (busAccessAllowed() && _file && *_file) {
|
|
_file->sync();
|
|
if (_sd->card()) _sd->card()->syncDevice();
|
|
}
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::reopen() {
|
|
_total = 0;
|
|
_planned_bootloader = false;
|
|
if (!mount()) return false;
|
|
if (*_file) _file->close();
|
|
if (!_file->open(PATH, O_RDWR)) return false;
|
|
uint8_t hdr[8];
|
|
if (_file->fileSize() < 13 || !_file->seekSet(0) ||
|
|
_file->read(hdr, sizeof(hdr)) != (int)sizeof(hdr) ||
|
|
memcmp(hdr, MOTA_MAGIC, 4) != 0) {
|
|
_file->close();
|
|
return false;
|
|
}
|
|
uint32_t total = rd_u32le(hdr + 4);
|
|
if (total < 13 || total != _file->fileSize()) {
|
|
_file->close();
|
|
return false;
|
|
}
|
|
_total = total;
|
|
uint8_t manifest[MOTA_MFL];
|
|
MotaManifest parsed;
|
|
if (total < 8u + sizeof(manifest) + 5u || !_file->seekSet(8u) ||
|
|
_file->read(manifest, sizeof(manifest)) != (int)sizeof(manifest) ||
|
|
!mota_parse_manifest(manifest, sizeof(manifest), parsed)) {
|
|
_total = 0;
|
|
_file->close();
|
|
return false;
|
|
}
|
|
if (parsed.is_bootloader()) {
|
|
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
|
const uint64_t payload_off = 8u + sizeof(manifest) +
|
|
(uint64_t)parsed.block_count * 4u;
|
|
if (!ota_bootloader_image_geometry_valid(parsed.image_size, parsed.payload_size) ||
|
|
payload_off + parsed.payload_size + 5u != total) {
|
|
_total = 0;
|
|
_file->close();
|
|
return false;
|
|
}
|
|
_planned_bootloader = true;
|
|
#else
|
|
_total = 0;
|
|
_file->close();
|
|
return false;
|
|
#endif
|
|
}
|
|
if (!locate_file()) {
|
|
_total = 0;
|
|
_file->close();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreSdNrf52::discard() {
|
|
_total = 0;
|
|
_planned_bootloader = false;
|
|
_first_sector = 0;
|
|
_allocated_sectors = 0;
|
|
clear_sd_auth_record();
|
|
if (!mount()) return false;
|
|
if (_file && *_file) _file->close();
|
|
if (_sd->exists(PATH) && !_sd->remove(PATH)) {
|
|
fail("SD OTA file removal failed");
|
|
return false;
|
|
}
|
|
if (_sd->exists(PATH)) {
|
|
fail("SD OTA file still exists after removal");
|
|
return false;
|
|
}
|
|
SdCard* const card = _sd->card();
|
|
if (card && !card->syncDevice()) {
|
|
fail("SD OTA removal sync failed");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void OtaStoreSdNrf52::clear() { (void)discard(); }
|
|
|
|
bool OtaStoreSdNrf52::approve_for_bootloader(
|
|
const uint8_t expected_boot_image_hash[32],
|
|
const uint8_t authorized_container_hash[32]) {
|
|
if (!_total || !finalize()) return false;
|
|
if (!authorized_container_hash) {
|
|
fail("missing authenticated SD container hash");
|
|
return false;
|
|
}
|
|
SdCard* const card = _sd ? _sd->card() : nullptr;
|
|
const uint32_t card_sectors = card ? card->sectorCount() : 0;
|
|
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
|
if (_planned_bootloader) {
|
|
SelfFwInfo fi;
|
|
if (!expected_boot_image_hash ||
|
|
_total != MOTA_NRF52_BOOT_CONTAINER_SIZE ||
|
|
!ota_self_firmware(fi) ||
|
|
!ota_bootloader_scratch_headroom_valid(
|
|
fi.valid, mota_nrf52_app_base(), fi.image_len,
|
|
OTA_BOOT_SCRATCH_START) ||
|
|
!ota_bootloader_live_bank_preserves_scratch(
|
|
mota_nrf52_app_base(), fi.image_len, OTA_BOOT_SCRATCH_START)) {
|
|
fail("running firmware/settings do not preserve boot scratch");
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
if (!write(8 + MOTA_OFF_APPROVAL, APPROVAL_YES, sizeof(APPROVAL_YES)) ||
|
|
!_file->sync() || !card || !card->syncDevice()) {
|
|
fail("SD approval write failed");
|
|
return false;
|
|
}
|
|
|
|
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
|
if (_planned_bootloader) {
|
|
uint8_t token[MOTA_SD_BOOT_TOKEN_LEN];
|
|
mota_sd_boot_token_encode(token, _total, expected_boot_image_hash);
|
|
|
|
// This page is outside the hash-valid live app and below InternalFS. The
|
|
// token is temporary: OTAFIX validates it against the current SD bytes,
|
|
// then consumes the page as the first bootloader scratch page.
|
|
flash_nrf5x_flush();
|
|
if (!flash_nrf5x_erase(OTA_BOOT_SCRATCH_START) ||
|
|
flash_nrf5x_write(OTA_BOOT_SCRATCH_START, token, sizeof(token)) < 0) {
|
|
fail("SD bootloader token flash write failed");
|
|
return false;
|
|
}
|
|
flash_nrf5x_flush();
|
|
if (memcmp((const void*)(uintptr_t)OTA_BOOT_SCRATCH_START,
|
|
token, sizeof(token)) != 0) {
|
|
fail("SD bootloader token flash verify failed");
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
uint8_t auth[MOTA_SD_AUTH_LEN];
|
|
const uint8_t purpose = _planned_bootloader
|
|
? MOTA_SD_AUTH_PURPOSE_BOOTLOADER : MOTA_SD_AUTH_PURPOSE_APP;
|
|
const uint8_t format = _planned_bootloader ? MOTA_BOOT_FORMAT_VER : MOTA_APP_FORMAT_VER;
|
|
if (!mota_sd_auth_encode(auth, purpose, format, _first_sector,
|
|
_allocated_sectors, _total, card_sectors,
|
|
authorized_container_hash) ||
|
|
!publish_sd_auth_record(auth)) {
|
|
clear_sd_auth_record();
|
|
fail("SD retained authorization publish failed");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
#endif // OTA_SEEDER_ONLY
|
|
|
|
} // namespace ota
|
|
} // namespace mesh
|
|
|
|
#endif
|