From 9415dc4107c021a2a8f7a58553962efc9b1c5fe4 Mon Sep 17 00:00:00 2001 From: Jared Dohrman Date: Thu, 14 May 2026 17:58:08 +1000 Subject: [PATCH] fix: retry SD archive recovery while stats are enabled --- eastmesh-docs/custom-cli.md | 2 +- eastmesh-docs/web-panel.md | 2 ++ examples/simple_repeater/MyMesh.cpp | 24 ++++++++++++++++++++++++ examples/simple_repeater/MyMesh.h | 2 ++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/eastmesh-docs/custom-cli.md b/eastmesh-docs/custom-cli.md index 294c0f54..fd9f2fa1 100644 --- a/eastmesh-docs/custom-cli.md +++ b/eastmesh-docs/custom-cli.md @@ -128,7 +128,7 @@ OK - `get web` - `get web.status`: shows whether the local HTTPS panel is available. -- `get web.stats.status`: shows whether the dedicated stats page and history subsystem are enabled, whether recent history is active, whether PSRAM-backed history is available, and whether the SD-backed archive is mounted. When enabled, the history capture now covers supported environment telemetry too, not just the original battery/radio series. GPS-active boards also record per-minute satellites samples for the `/stats` history view. +- `get web.stats.status`: shows whether the dedicated stats page and history subsystem are enabled, whether recent history is active, whether PSRAM-backed history is available, and whether the SD-backed archive is mounted. When enabled, the history capture now covers supported environment telemetry too, not just the original battery/radio series. GPS-active boards also record per-minute satellites samples for the `/stats` history view. If archive access drops while stats remain enabled, the repeater retries the SD mount periodically. - `set web on|off` - `set.web on|off`: enables or disables the local HTTPS panel. - `set web.stats on|off` diff --git a/eastmesh-docs/web-panel.md b/eastmesh-docs/web-panel.md index e4d41140..befc15c3 100644 --- a/eastmesh-docs/web-panel.md +++ b/eastmesh-docs/web-panel.md @@ -311,6 +311,8 @@ When `web.stats` is enabled, stats history starts capturing from boot or from th Archive-backed restore requires `web.stats` enabled plus a mounted SD card on boards that support the EastMesh archive path. +If archive access drops at runtime, the repeater retries the SD mount periodically while `web.stats` remains enabled. + The main purpose of the SD card is to let the repeater retain and restore stats history for `/stats`. The archive keeps fast `.latest` snapshot files for quick restore and UTC-dated daily `.log` files for longer-term history. As a secondary option, those files can also be removed and inspected on a computer for deeper manual review. On no-PSRAM boards, `/stats` can still show recent graphs, but the history is smaller and does not provide the same archive-backed behaviour as PSRAM-capable boards. diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index dc7c0520..aadbb4ae 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -188,6 +188,7 @@ bool appendJsonFloatField(char* reply, size_t reply_size, size_t& offset, bool& } constexpr unsigned long kArchiveNeighboursFlushIntervalMs = 60UL * 1000UL; +constexpr unsigned long kArchiveRecoveryRetryIntervalMs = 5UL * 60UL * 1000UL; constexpr const char* kArchiveNeighboursSnapshotPath = "/stats/neighbours.snapshot"; constexpr const char* kArchiveNeighboursLatestPath = "/stats/neighbours.latest"; @@ -1201,6 +1202,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _archive = nullptr; uptime_millis = 0; next_archive_neighbours_flush_ms = 0; + next_archive_recover_ms = 0; next_battery_sample_ms = 0; next_history_sample_ms = 0; next_local_advert = next_flood_advert = 0; @@ -1789,9 +1791,11 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) { if (!_stats_history.isEnabled()) { _stats_state.initialized = false; _archive_neighbours_dirty = false; + next_archive_recover_ms = 0; return; } _stats_history.maybeReleaseIdleBuffers(now_ms); + maybeRecoverArchive(now_ms); const bool wifi_connected = network.isWifiConnected(); #ifdef WITH_MQTT_UPLINK @@ -1970,6 +1974,7 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) { } _stats_history.maybeFlush(now_ms); + maybeRecoverArchive(now_ms); if (!_stats_history.isLiveOnly()) { maybeFlushArchiveNeighbours(now_ms); } @@ -1978,6 +1983,25 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) { #endif } +void MyMesh::maybeRecoverArchive(unsigned long now_ms) { +#if defined(ESP_PLATFORM) && WITH_WEB_PANEL + if (_archive == nullptr || _archive->isMounted()) { + next_archive_recover_ms = 0; + return; + } + if (next_archive_recover_ms != 0 && !millisHasNowPassed(next_archive_recover_ms)) { + return; + } + if (!_archive->recover()) { + next_archive_recover_ms = now_ms + kArchiveRecoveryRetryIntervalMs; + return; + } + next_archive_recover_ms = 0; +#else + (void)now_ms; +#endif +} + bool MyMesh::appendJsonEvents(char* reply, size_t reply_size, size_t& offset) const { offset += snprintf(&reply[offset], reply_size - offset, "\"events\":["); const size_t max_events = min(_stats_history.getEventCount(), 6); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index da51a500..5a80741a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -129,6 +129,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm uint32_t last_millis; uint64_t uptime_millis; unsigned long next_archive_neighbours_flush_ms; + unsigned long next_archive_recover_ms; unsigned long next_battery_sample_ms; unsigned long next_history_sample_ms; unsigned long next_local_advert, next_flood_advert; @@ -192,6 +193,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); size_t getNeighbourCount() const; void updateStatsHistory(unsigned long now_ms); + void maybeRecoverArchive(unsigned long now_ms); bool restoreArchiveNeighbours(); void flushArchiveNeighbours(); void maybeFlushArchiveNeighbours(unsigned long now_ms);