From f22c9decc5b4afe68f019d62216ba8affd6cd476 Mon Sep 17 00:00:00 2001 From: Dan Vybiral Date: Sat, 25 Jul 2026 00:19:51 +0200 Subject: [PATCH] touch: raise SD max_files 3 -> 6; report chat-save failure stage + errno Field evidence reframed the chat-save failures: a soft reboot wrote the FULL 515 KB ring to the same card without trouble, while the periodic worker autosave could not even create the file. That rules out a size/bad-region card problem and points at contention: the SD VFS was mounted with max_files=3 (set when the card only served the file manager), but the card now serves telemetry log rewrites (2 handles), battery log rewrites (2), the history flush, tile reads/writes, WAV chimes and the About readout concurrently -- the flush's open("w") loses that race with ENFILE while the card itself is fine, and at reboot (everything quiesced) the same write sails through. All four mount sites now use max_files=6 (~+1.7 KB while mounted). To make the next such failure self-explaining on a device with no readable serial, the About Chat-store line now includes the failure stage and errno ('FAIL x3 @o e23'): o/h/b/r = open/header/body/rename; errno 23/24 = VFS file table full, 28 = card full, 5 = card I/O error. The header-write failure path also joins the failure accounting (it returned early without counting before) and cleans up its temp file. Builds verified: all five SD-relevant touch envs. Signed-off-by: Dan Vybiral --- src/main.cpp | 2 +- src/ui-touch/UITask.cpp | 43 +++++++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 6b481deb..3cca636a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -520,7 +520,7 @@ void setup() { for (int a = 0; a < tries && !sd_mounted; ++a) { SD.end(); delay(kBootMount[a].settle_ms); - if (SD.begin(PIN_SD_CS, *_spi, kBootMount[a].hz, "/sd", 3) && SD.cardType() != CARD_NONE) + if (SD.begin(PIN_SD_CS, *_spi, kBootMount[a].hz, "/sd", 6) && SD.cardType() != CARD_NONE) sd_mounted = true; } } diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 474e6586..20344970 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -18,6 +18,7 @@ #include #include #include +#include // chat-store write diagnostics surface errno (ENFILE vs ENOSPC vs EIO) #if defined(ESP32) #include #include @@ -4559,6 +4560,8 @@ static bool uiHistWorkerFlush(); // defined with the storage cod static volatile uint32_t s_msgs_write_ok_ms = 0; // millis() of last successful ring write (0 = none yet) static volatile uint32_t s_msgs_write_fail_ms = 0; // millis() of last failed ring write static volatile uint16_t s_msgs_write_fails = 0; // consecutive failures since the last success +static volatile uint8_t s_msgs_write_stage = 0; // where the last failure hit: 'o' open, 'h' header, 'b' body, 'r' rename +static volatile int s_msgs_write_errno = 0; // errno at that failure (ENFILE/EMFILE = VFS file table full, ENOSPC = card full, EIO = card error) static bool uiDataFsIsSdCard(); // fwd (storage code below) — About page shows the resolved backend static File uiDataOpen(const char* name, const char* mode); // fwd — About page reads the msgs file size static volatile bool s_sdinfo_done = false; // worker -> UI: a result exists @@ -10068,8 +10071,12 @@ static void sysInfoTextLive(char* buf, size_t cap) { } char stat[48]; if (s_msgs_write_fails) - snprintf(stat, sizeof stat, "FAILING x%u (%lus ago)", + // @stage: o=open h=header b=body r=rename; errno: 23/24=file table full, + // 28=card full, 5=I/O error (card) — the triage key for save failures. + snprintf(stat, sizeof stat, "FAIL x%u @%c e%d (%lus ago)", (unsigned)s_msgs_write_fails, + s_msgs_write_stage ? (char)s_msgs_write_stage : '?', + (int)s_msgs_write_errno, (unsigned long)((nowms - s_msgs_write_fail_ms) / 1000u)); else if (s_msgs_write_ok_ms) snprintf(stat, sizeof stat, "OK %lus ago", @@ -18124,8 +18131,11 @@ static bool fmSdTryMount() { // mounts on the first attempt and returns immediately; only a flaky/cold card // walks the whole ladder (~1.1 s worst case). The cumulative delay can // approach the loop watchdog window, so drop the WDT around the loop. 4 MHz - // is reliable on the shared LoRa bus; max_files=3 keeps the VFS footprint - // small (dir + at most 2 files). + // is reliable on the shared LoRa bus. max_files=6 (was 3): the card now + // serves telemetry log rewrites (2 handles), battery log rewrites (2), + // the history flush, tile reads/writes, WAV chimes and the About readout + // CONCURRENTLY — with only 3 VFS slots the history flush's open("w") lost + // that race and chat saves failed with ENFILE while the card was fine. // Ladder of (settle, clock) attempts. The first three use the fast 4 MHz the // shared LoRa bus handles well; a cold / cheap card that won't wake at 4 MHz // then gets progressively longer settles AND a lower clock (1 MHz, then @@ -18150,7 +18160,7 @@ static bool fmSdTryMount() { for (int attempt = 0; attempt < kAttempts; ++attempt) { SD.end(); delay(kMountLadder[attempt].settle_ms); - if (SD.begin(PIN_SD_CS, *spi, kMountLadder[attempt].hz, "/sd", 3) && SD.cardType() != CARD_NONE) { + if (SD.begin(PIN_SD_CS, *spi, kMountLadder[attempt].hz, "/sd", 6) && SD.cardType() != CARD_NONE) { mounted = true; break; } @@ -41435,14 +41445,24 @@ static bool uiMsgsWriteResult(bool ok) { } return ok; } +// Failure wrapper: record WHERE the write died and the errno — 'open failed +// with ENFILE' (VFS file-handle table full) needs a completely different fix +// than 'body write failed with EIO' (card), and without this they were +// indistinguishable on a device with no readable serial. +static bool uiMsgsWriteFail(char stage) { + s_msgs_write_stage = (uint8_t)stage; + s_msgs_write_errno = errno; + return uiMsgsWriteResult(false); +} static bool uiWriteMsgsFile(const UITask::UIMessage* msgs, int cap, uint16_t count, uint16_t head, uint32_t msgcount, const char* tmp_path) { #if defined(ESP32) WdtHeavyGuard _wg; // a fragmenting write can trigger a multi-second SPIFFS GC + errno = 0; File f = uiDataOpen(tmp_path, "w"); - if (!f) return uiMsgsWriteResult(false); + if (!f) return uiMsgsWriteFail('o'); UiMsgFileHeader hdr{}; hdr.magic = k_ui_msgs_magic; @@ -41471,7 +41491,8 @@ static bool uiWriteMsgsFile(const UITask::UIMessage* msgs, int cap, hdr.ui_msg_head = 0; // written oldest-first => the file's ring head sits at 0 hdr.msgcount = msgcount; if (f.write(reinterpret_cast(&hdr), sizeof(hdr)) != sizeof(hdr)) { - f.close(); return false; + f.close(); uiDataRemove(tmp_path); + return uiMsgsWriteFail('h'); // was silently un-counted before } const size_t REC = sizeof(UiHistoryMsg); @@ -41530,8 +41551,10 @@ static bool uiWriteMsgsFile(const UITask::UIMessage* msgs, int cap, } } f.close(); - if (!ok) { uiDataRemove(tmp_path); return uiMsgsWriteResult(false); } - return uiMsgsWriteResult(uiDataReplaceFile(k_ui_msgs_path, tmp_path)); + if (!ok) { uiDataRemove(tmp_path); return uiMsgsWriteFail('b'); } + errno = 0; + if (!uiDataReplaceFile(k_ui_msgs_path, tmp_path)) return uiMsgsWriteFail('r'); + return uiMsgsWriteResult(true); #else (void)msgs; (void)cap; (void)count; (void)head; (void)msgcount; (void)tmp_path; return false; @@ -44273,7 +44296,7 @@ static void sdHealthTick() { SPIClass* spi = sdSharedSPI(); if (!spi) return; SD.end(); - if (SD.begin(PIN_SD_CS, *spi, 4000000, "/sd", 3) && SD.cardType() != CARD_NONE) { + if (SD.begin(PIN_SD_CS, *spi, 4000000, "/sd", 6) && SD.cardType() != CARD_NONE) { s_sd_mounted = true; s_sd_size = SD.cardSize(); s_sd_retry_after_ms = 0; @@ -45281,7 +45304,7 @@ void UITask::loop() { } sdcard_uninit(pdrv); } - if (ok) ok = SD.begin(PIN_SD_CS, *spi, 4000000, "/sd", 3); + if (ok) ok = SD.begin(PIN_SD_CS, *spi, 4000000, "/sd", 6); } enableLoopWDT(); fmHideFormatOverlay();