Files
MeshCore/src/helpers/SdCardCLI.cpp
T

272 lines
9.9 KiB
C++

#include "SdCardCLI.h"
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
#include <Arduino.h>
#include <MeshCore.h>
#include <stdlib.h>
#include <string.h>
#include <helpers/CLICommandUtils.h>
#include <helpers/StorageLayout.h>
#include <helpers/ota/OtaContext.h>
namespace mesh {
namespace cli {
static const uint32_t SDCARD_ACTION_COOLDOWN_MILLIS = 5UL * 60UL * 1000UL;
static const char* skipSpacesConst(const char* value) {
while (*value == ' ' || *value == '\t') ++value;
return value;
}
static bool sdCardBusAvailable(char* reply, size_t capacity) {
#if defined(OTA_RAK15002_SD_STORE)
if (mesh::ota::rak4631_ethernet_owns_spi()) {
mesh::cli::formatError(reply, capacity, "turn Ethernet off before accessing SD");
return false;
}
#else
(void)reply;
(void)capacity;
#endif
return true;
}
static void formatSdCardElapsed(char* out, size_t out_len, bool recorded,
uint32_t ran_at, uint32_t now) {
if (!recorded) {
snprintf(out, out_len, "never");
return;
}
const uint32_t elapsed_secs = (now - ran_at) / 1000UL;
if (elapsed_secs < 60UL) {
snprintf(out, out_len, "%lus ago", (unsigned long)elapsed_secs);
} else if (elapsed_secs < 60UL * 60UL) {
snprintf(out, out_len, "%lum %lus ago",
(unsigned long)(elapsed_secs / 60UL),
(unsigned long)(elapsed_secs % 60UL));
} else if (elapsed_secs < 24UL * 60UL * 60UL) {
snprintf(out, out_len, "%luh %lum ago",
(unsigned long)(elapsed_secs / 3600UL),
(unsigned long)((elapsed_secs / 60UL) % 60UL));
} else {
snprintf(out, out_len, "%lud %luh ago",
(unsigned long)(elapsed_secs / 86400UL),
(unsigned long)((elapsed_secs / 3600UL) % 24UL));
}
}
bool handleSdCardSet(SdCardCLIState& state, MainBoard& board,
const char* config, char* reply, size_t capacity) {
if (!config || !reply || capacity == 0
|| strncmp(config, "sdcard", 6) != 0 ||
(config[6] != 0 && config[6] != ' ')) {
return false;
}
const char* action_args = skipSpacesConst(config + 6);
bool is_format;
bool force = false;
if (strcmp(action_args, "format") == 0) {
is_format = true;
} else if (strcmp(action_args, "format --force") == 0) {
is_format = true;
force = true;
} else if (strcmp(action_args, "erase") == 0) {
is_format = false;
} else if (strcmp(action_args, "erase --force") == 0) {
is_format = false;
force = true;
} else {
mesh::cli::formatError(reply, capacity, "use set sdcard format|erase [--force]");
return true;
}
const bool recorded = is_format ? state.format_recorded : state.erase_recorded;
const uint32_t ran_at = is_format ? state.format_at : state.erase_at;
const char* action = is_format ? "format" : "erase";
const uint32_t now = millis();
if (!force && recorded && now - ran_at < SDCARD_ACTION_COOLDOWN_MILLIS) {
char age[32];
formatSdCardElapsed(age, sizeof(age), true, ran_at, now);
snprintf(reply, capacity,
"Error: SD card %s ran %s; use set sdcard %s --force",
action, age, action);
return true;
}
if (!sdCardBusAvailable(reply, capacity)) 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, capacity)) return true;
mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
if (context.apply_pending) {
mesh::cli::formatError(reply, capacity, "OTA apply is pending");
return true;
}
#if defined(OTA_TOWER_AUTO_STORE)
// Card maintenance must never abandon a fallback update stored internally.
if (context.fetch_store.usesInternal() &&
(context.manager.fetchState() != mesh::ota::OtaManager::IDLE ||
context.fetch_store.staged_size() != 0)) {
mesh::cli::formatError(reply, capacity, "internal OTA slot is occupied; use ota cancel first");
return true;
}
#elif defined(OTA_RAK15002_SD_STORE) && !defined(OTA_SEEDER_ONLY)
// The optional card is independent of an internal/NOR application fetch
// or an internal bootloader package. Formatting it must not reset that
// unrelated receive session or strand its still-pinned staging bytes.
if (!context.fetch_store.usesSd() &&
(context.manager.fetchState() != mesh::ota::OtaManager::IDLE ||
context.fetch_store.staged_size() != 0)) {
mesh::cli::formatError(reply, capacity, "non-SD OTA slot is occupied; use ota cancel first");
return true;
}
#endif
mesh::ota::OtaStoreSdNrf52& card = context.sdStagingStore();
// A card-wide destructive operation invalidates the staged SD fetch. First
// let an archive capture checkpoint and close its shared card handles.
context.prepareSdCardReset();
context.manager.reset_session();
context.manager.want(0);
context.manager.want_mid(nullptr);
context.session_started_ms = 0;
context.prev_fstate = mesh::ota::OtaManager::IDLE;
if (is_format) {
if (!card.formatCard(board)) {
char error[80];
strncpy(error, card.last_error(), sizeof(error) - 1);
error[sizeof(error) - 1] = 0;
context.finishSdCardReset(millis());
snprintf(reply, capacity, "Error: SD card format failed: %s",
error);
return true;
}
context.finishSdCardReset(millis());
state.format_recorded = true;
state.format_at = millis();
snprintf(reply, capacity, "OK - SD card format complete");
return true;
}
if (!card.eraseCard(board)) {
char error[80];
strncpy(error, card.last_error(), sizeof(error) - 1);
error[sizeof(error) - 1] = 0;
context.finishSdCardReset(millis());
snprintf(reply, capacity, "Error: SD card erase failed: %s",
error);
return true;
}
state.erase_recorded = true;
state.erase_at = millis();
if (!card.formatCard(board)) {
char error[80];
strncpy(error, card.last_error(), sizeof(error) - 1);
error[sizeof(error) - 1] = 0;
context.finishSdCardReset(millis());
snprintf(reply, capacity, "Error: SD card erased but format failed: %s",
error);
return true;
}
state.format_recorded = true;
state.format_at = millis();
context.finishSdCardReset(millis());
snprintf(reply, capacity, "OK - SD card erase and format complete");
return true;
}
bool handleSdCardGet(SdCardCLIState& state, MainBoard& board,
const char* config, char* reply, size_t capacity) {
if (!config || !reply || capacity == 0
|| strncmp(config, "sdcard", 6) != 0 ||
(config[6] != 0 && config[6] != ' ')) {
return false;
}
const char* query = skipSpacesConst(config + 6);
const uint32_t now = millis();
if (*query == 0 || strcmp(query, "*") == 0) {
char format_age[32];
char erase_age[32];
formatSdCardElapsed(format_age, sizeof(format_age),
state.format_recorded, state.format_at, now);
formatSdCardElapsed(erase_age, sizeof(erase_age),
state.erase_recorded, state.erase_at, now);
snprintf(reply, capacity, "> format: %s, erase: %s", format_age, erase_age);
} else if (strcmp(query, "format") == 0) {
char age[32];
formatSdCardElapsed(age, sizeof(age), state.format_recorded,
state.format_at, now);
snprintf(reply, capacity, "> format: %s", age);
} else if (strcmp(query, "erase") == 0) {
char age[32];
formatSdCardElapsed(age, sizeof(age), state.erase_recorded,
state.erase_at, now);
snprintf(reply, capacity, "> erase: %s", age);
} else if (strcmp(query, "free") == 0) {
if (!sdCardBusAvailable(reply, capacity)) return true;
if (!mesh::ota::ota_acquire_context(reply, capacity)) return true;
uint64_t used_bytes = 0;
uint64_t free_bytes = 0;
mesh::ota::OtaStoreSdNrf52& store = mesh::ota::ota_ctx().sdStagingStore();
if (!store.getSpace(board, used_bytes, free_bytes)) {
snprintf(reply, capacity, "Error: SD card space query failed: %s",
store.last_error());
} else {
char used[24];
char free[24];
mesh::cli::formatStorageBytes(used, sizeof(used), used_bytes);
mesh::cli::formatStorageBytes(free, sizeof(free), free_bytes);
snprintf(reply, capacity, "> used: %s, free: %s", used, free);
}
} else if (strncmp(query, "ls", 2) == 0 || strncmp(query, "dir", 3) == 0) {
const bool is_ls = strncmp(query, "ls", 2) == 0;
const size_t word_len = is_ls ? 2 : 3;
if (query[word_len] != 0 && query[word_len] != ' ') {
mesh::cli::formatError(reply, capacity, "use get sdcard ls|dir [page]");
return true;
}
const char* page_arg = skipSpacesConst(query + word_len);
uint32_t page = 1;
if (*page_arg) {
char* end = nullptr;
unsigned long parsed = strtoul(page_arg, &end, 10);
if (parsed == 0 || parsed > 65535 || !end || *skipSpacesConst(end) != 0) {
mesh::cli::formatError(reply, capacity, "use get sdcard ls|dir [page]");
return true;
}
page = parsed;
}
if (!sdCardBusAvailable(reply, capacity)) return true;
if (!mesh::ota::ota_acquire_context(reply, capacity)) return true;
mesh::ota::OtaStoreSdNrf52& store = mesh::ota::ota_ctx().sdStagingStore();
if (!store.listFiles(board, (uint16_t)page, reply, capacity)) {
snprintf(reply, capacity, "Error: SD card list failed: %s", store.last_error());
}
} else {
mesh::cli::formatError(reply, capacity, "use get sdcard [format|erase|free|ls|dir [page]|*]");
}
return true;
}
bool handleSdCardCommand(SdCardCLIState& state, MainBoard& board,
const char* command, char* reply, size_t capacity) {
if (!command) return false;
if (strncmp(command, "get ", 4) == 0)
return handleSdCardGet(state, board, command + 4, reply, capacity);
if (strncmp(command, "set ", 4) == 0)
return handleSdCardSet(state, board, command + 4, reply, capacity);
return false;
}
} // namespace cli
} // namespace mesh
#endif