#include "SdCardCLI.h" #if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) #include #include #include #include #include #include #include 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