diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 736df7e..b9f0a45 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -41853,6 +41853,12 @@ static bool uiDataFsReady() { // T-Deck's /meshcomod root; fall back to FFat only when no card is present. Both are mounted at // boot in main.cpp (g_sd_ok / g_fs_ok). (The P4 used to fall into the #else SPIFFS branch below — // no SPIFFS partition exists there, so history was never persisted and vanished on every reboot.) + // #167: hot UI data (chat history) lives on the INTERNAL LittleFS -- SD write bursts + // electrically disturb this board's AMOLED, and the P4's internal FAT layer is broken + // (see the storage note in tdisplay_p4/main/main.cpp). SD = degraded fallback only; + // tiles keep using the SD via their own selector. + extern bool g_fs_ok; + if (g_fs_ok) { s_ui_data_fs = &LittleFS; s_ui_data_root[0] = '\0'; return true; } extern bool g_sd_ok; if (g_sd_ok) { SD_MMC.mkdir("/meshcomod"); @@ -41860,8 +41866,6 @@ static bool uiDataFsReady() { strncpy(s_ui_data_root, "/meshcomod", sizeof s_ui_data_root - 1); return true; } - extern bool g_fs_ok; - if (g_fs_ok) { s_ui_data_fs = &FFat; s_ui_data_root[0] = '\0'; return true; } #elif defined(HAS_TDECK_GT911) // T-Deck: the SD card is the persistent user-data store (large, removable, // survives a reflash) and is where chat history already lives. Prefer it; only diff --git a/tdisplay_p4/main/CMakeLists.txt b/tdisplay_p4/main/CMakeLists.txt index 6bbb37f..73a9bf4 100644 --- a/tdisplay_p4/main/CMakeLists.txt +++ b/tdisplay_p4/main/CMakeLists.txt @@ -111,6 +111,32 @@ endforeach() # One-off C6-reflash helper: build with `C6_FLASH_HELPER=1 ./build.sh build` to boot a P4 image that # never touches the C6 (no C6_EN reset pulse, no AT worker), so the C6 can be held in ROM download mode # and reflashed over its own USB. Harmless when the env var is unset (normal builds are unaffected). +# #167 SD-coupling experiments: WADA_SD_KHZ= / WADA_SD_1BIT=1 derate the primary SD mount +# (clock and/or bus width) to isolate + mitigate the SD-activity screen flash. +if(DEFINED ENV{WADA_SD_KHZ}) + idf_build_set_property(COMPILE_OPTIONS "-DTDP4_SD_KHZ=$ENV{WADA_SD_KHZ}" APPEND) + message(STATUS "wadamesh: SD primary rung clock = $ENV{WADA_SD_KHZ} kHz") +endif() +if(DEFINED ENV{WADA_SD_1BIT}) + idf_build_set_property(COMPILE_OPTIONS "-DTDP4_SD_1BIT=true" APPEND) + message(STATUS "wadamesh: SD primary rung = 1-bit bus") +endif() + +# #167: WADA_DSI_MBPS= overrides the RM69A10 DSI lane bit rate (default 750 -- see the +# RM_BITRATE note in RM69A10Display.cpp; 1000 = the LilyGo/Meck value with the thin eye). +if(DEFINED ENV{WADA_DSI_MBPS}) + idf_build_set_property(COMPILE_OPTIONS "-DRM_BITRATE_MBPS=$ENV{WADA_DSI_MBPS}" APPEND) + message(STATUS "wadamesh: DSI lane rate = $ENV{WADA_DSI_MBPS} Mbps") +endif() + +# Companion diagnostic (#167): `WADA_P4_POKE_TRACE=1 ./build.sh build` logs every runtime poke at +# the panel or the XL9535 expander (DCS brightness writes, disp on/off, expander bit writes) so +# visible flashes can be correlated against them on the console. +if(DEFINED ENV{WADA_P4_POKE_TRACE}) + idf_build_set_property(COMPILE_OPTIONS "-DTDP4_POKE_TRACE=1" APPEND) + message(STATUS "wadamesh: POKE TRACE enabled (diagnostic build, do not ship)") +endif() + # Diagnostic build for the whole-screen colour flash (#167): `WADA_P4_FLUSH_TRACE=1 ./build.sh build` # logs every large, mostly-single-colour band pushed to the panel, with its RGB565 value. If the # screen flashes and nothing logs, the cause is below LVGL and the UI is the wrong place to look. diff --git a/tdisplay_p4/main/main.cpp b/tdisplay_p4/main/main.cpp index 8a7f393..24d4732 100644 --- a/tdisplay_p4/main/main.cpp +++ b/tdisplay_p4/main/main.cpp @@ -14,7 +14,12 @@ #include "esp_heap_caps.h" #include "UITask.h" #include "target.h" // board, radio_driver, rtc_clock, display, sensors, radio_init() -#include // internal 'storage' FAT partition (no-card fallback) +#include // internal 'storage' partition, mounted as LittleFS (label lookup is + // subtype-agnostic, so the ex-FAT partition converts in place -- no + // partition-table change, OTA-safe). The P4's FAT-on-flash layer has + // documented broken metadata (exists/size/f_getfree lie) and v3 of the + // #167 migration proved even its WRITES cannot be trusted; LittleFS is + // the same battle-proven backend every S3 board uses internally. #include // microSD on SDMMC slot 0 (primary store) #include "esp_partition.h" #include @@ -72,8 +77,8 @@ volatile int g_boot_phase = 0; extern "C" void set_boot_phase(int phase) { g_boot_phase = phase; } // App globals — src/main.cpp declares these; we own them here (that file is excluded from the build). -DataStore store(FFat, rtc_clock); -bool g_fs_ok = false; // FFat('storage') mounted (extern — UITask file browser) +DataStore store(LittleFS, rtc_clock); +bool g_fs_ok = false; // internal 'storage' (LittleFS) mounted (extern — UITask file browser) bool g_sd_ok = false; // microSD mounted (extern) MultiTransportCompanionInterface serial_interface; StdRNG fast_rng; @@ -99,9 +104,11 @@ static void hostedConnectC6() { // Recursive FS→FS copy for the one-time SD adoption migration (FFat store → SD /meshcomod). // Skips files that already exist at the destination, so a partial earlier run just completes. -static void migrateFsCopyFile(fs::FS &src, const char *sp, fs::FS &dst, const char *dp) { - File probe = dst.open(dp, FILE_READ); - if (probe) { probe.close(); return; } +static void migrateFsCopyFile(fs::FS &src, const char *sp, fs::FS &dst, const char *dp, bool overwrite = false) { + if (!overwrite) { + File probe = dst.open(dp, FILE_READ); + if (probe) { probe.close(); return; } + } File in = src.open(sp, FILE_READ); if (!in) return; File out = dst.open(dp, FILE_WRITE); @@ -112,7 +119,7 @@ static void migrateFsCopyFile(fs::FS &src, const char *sp, fs::FS &dst, const ch out.close(); in.close(); printf("[storage] migrated %s\n", sp); } -static void migrateFsDir(fs::FS &src, const char *sdir, fs::FS &dst, const char *ddir) { +static void migrateFsDir(fs::FS &src, const char *sdir, fs::FS &dst, const char *ddir, bool overwrite = false) { File d = src.open(sdir); if (!d || !d.isDirectory()) { if (d) d.close(); return; } dst.mkdir(ddir); @@ -126,8 +133,8 @@ static void migrateFsDir(fs::FS &src, const char *sdir, fs::FS &dst, const char snprintf(dp, sizeof dp, "%s/%s", ddir, leaf); bool is_dir = f.isDirectory(); f.close(); - if (is_dir) migrateFsDir(src, sp, dst, dp); - else migrateFsCopyFile(src, sp, dst, dp); + if (is_dir) migrateFsDir(src, sp, dst, dp, overwrite); + else migrateFsCopyFile(src, sp, dst, dp, overwrite); } d.close(); } @@ -164,9 +171,12 @@ static void wadameshSetup() { DisplayDriver* disp = &display; // 6. Storage — microSD (SDMMC slot 0) primary, internal FFat 'storage' as the no-card fallback. - g_fs_ok = FFat.begin(true, "/ffat", 10, "storage"); - printf("[storage] FFat(storage) = %s\n", g_fs_ok ? "OK" : "FAILED"); - if (g_fs_ok) { FFat.mkdir("/identity"); FFat.mkdir("/bl"); } + // formatOnFail=true: the first boot of this build finds FAT data in the partition, fails the + // littlefs mount, and formats it — the deliberate one-way conversion (store contents come back + // from the SD card via the v4 migration below). + g_fs_ok = LittleFS.begin(true, "/lfs", 10, "storage"); + printf("[storage] LittleFS(storage) = %s\n", g_fs_ok ? "OK" : "FAILED"); + if (g_fs_ok) { LittleFS.mkdir("/identity"); LittleFS.mkdir("/bl"); } // The slot's VDD is gated by the XL9535's SD_EN (IO15), ACTIVE-LOW — powerOnSequence() drives it // low. (Root-caused 2026-07-15 with a GPIO pad sweep: SD_EN high = all six SD pads clamped LOW // through the unpowered card's ESD diodes -> 0x107/0x109 on every mount; SD_EN low = pads high, @@ -180,9 +190,24 @@ static void wadameshSetup() { for (int p : {44, 39, 40, 41, 42}) gpio_pullup_en((gpio_num_t)p); // Mount ladder: 4-bit@20 MHz normally succeeds first try now that the slot is powered; the // fallbacks stay for marginal cards. (1-bit skips D1..D3; 5 MHz derates signal margin.) + // + // #167: SD activity IS the whole-screen flash. Proven by elimination on-device: identical + // firmware with the card removed never flashes, while every reported trigger (message arrives, + // opening an unread chat, contact add/delete, boot pre-splash) ends in an SD write burst or the + // mount probe. The display stack itself measured clean throughout (framebuffer content, bridge + // underrun, host protocol errors, DCS traffic, expander writes -- all silent during visible + // flashes; DSI config + panel init identical to the flash-free Meck-P4). Remaining coupling is + // electrical: the 4-bit 20 MHz SDMMC bus and/or the card's write-current spikes on the shared + // rail. TDP4_SD_KHZ / TDP4_SD_1BIT (build-time) tune the first mount rung to test/derate. { +#ifndef TDP4_SD_KHZ +#define TDP4_SD_KHZ SDMMC_FREQ_DEFAULT // 20 MHz +#endif +#ifndef TDP4_SD_1BIT +#define TDP4_SD_1BIT false +#endif struct { bool onebit; int khz; const char *tag; } tries[] = { - { false, SDMMC_FREQ_DEFAULT, "4-bit 20MHz" }, + { TDP4_SD_1BIT, TDP4_SD_KHZ, "primary (TDP4_SD_KHZ)" }, { false, 5000, "4-bit 5MHz" }, { true, SDMMC_FREQ_DEFAULT, "1-bit 20MHz" }, { true, 5000, "1-bit 5MHz" }, @@ -197,20 +222,71 @@ static void wadameshSetup() { } printf("[storage] SD_MMC = %s\n", g_sd_ok ? "OK" : "no card"); if (g_sd_ok) { - SD_MMC.mkdir("/meshcomod"); SD_MMC.mkdir("/meshcomod/identity"); SD_MMC.mkdir("/meshcomod/bl"); - // One-time ADOPTION MIGRATION (the beta_36 lesson: never flip a storage root without migrating). - // This device has been running with the store on FFat while the SD init was broken — if the card - // has no identity yet but FFat does, copy the whole FFat store across BEFORE switching the root, - // or the node would boot with a fresh identity and "lose" contacts/prefs/history. - File sp = SD_MMC.open("/meshcomod/identity/_main.id", FILE_READ); - bool sd_has_id = (bool)sp; if (sp) sp.close(); - File fp = FFat.open("/identity/_main.id", FILE_READ); - bool ff_has_id = (bool)fp; if (fp) fp.close(); - if (!sd_has_id && ff_has_id) { - printf("[storage] first SD adoption -> migrating FFat store to SD /meshcomod\n"); - migrateFsDir(FFat, "/", SD_MMC, "/meshcomod"); + // #167 RESOLUTION -- the hot store lives on INTERNAL FFat; the SD carries only bulk, gentle + // writers (map tiles, backups). Root cause, established by on-device elimination: SD-card + // WRITE bursts electrically disturb the AMOLED (whole-screen blue flash). The display stack + // measured clean throughout -- framebuffer content, DSI bridge underrun, DSI host protocol + // errors, DCS traffic, expander writes: all silent during visible flashes -- and the flash + // survived every digital derate (4->1-bit bus, 20->10 MHz SD clock, 1000->750 Mbps DSI lane + // rate) on EVERY card tried, at any brightness, yet vanished the moment the card was removed + // or left unwritten. The card's supply rail is shared with the panel (XL9535 SD_EN switches + // it off the same 3V3 domain), so its program-current spikes reach the panel and firmware + // cannot filter them -- it can only stop hammering the card. Streaming tile writes never + // flashed, so the SD keeps those. This is also why the flash-free Meck build proves nothing + // about the board: it keeps its store internal and never writes the SD at all. + // + // One-time REVERSE migration: earlier P4 builds kept the live store on SD /meshcomod, so if + // the card carries an identity and this build hasn't migrated yet, copy the store back to + // FFat, OVERWRITING FFat's stale pre-adoption copies (the SD is the current truth -- booting + // cardless on the old FFat set showed long-deleted contacts). SD data is left in place, + // untouched, as a rollback. + // v4 migration -- one lesson per predecessor: v1 trusted whichever card was in and copied a + // blank store; v2 copied the whole SD tree and filled the partition before contacts3 landed; + // v3 did format+whitelist correctly and STILL read back empty -- because the P4's FAT-on-flash + // driver itself is broken (its stat layer lying was already documented; v3 proved writes are + // no better). v4 = the same clean-slate whitelist copy, onto LittleFS. Card is never written. + File mk = LittleFS.open("/.hot_store_on_lfs_v4", FILE_READ); + bool migrated = (bool)mk; if (mk) mk.close(); + if (!migrated) { + File sp = SD_MMC.open("/meshcomod/identity/_main.id", FILE_READ); + bool sd_has_id = (bool)sp; if (sp) sp.close(); + auto fsize = [](fs::FS &f, const char *path) -> size_t { + File h = f.open(path, FILE_READ); size_t n = h ? h.size() : 0; if (h) h.close(); return n; + }; + size_t sd_meshcomod = fsize(SD_MMC, "/meshcomod/contacts3"); + size_t sd_root = fsize(SD_MMC, "/contacts3"); + size_t lf_contacts = fsize(LittleFS, "/contacts3"); + size_t sd_contacts = sd_meshcomod > sd_root ? sd_meshcomod : sd_root; + if (sd_has_id && sd_contacts > lf_contacts) { + printf("[storage] #167 v4: store SD -> LittleFS (contacts3 /meshcomod=%uB root=%uB, lfs had %uB)\n", + (unsigned)sd_meshcomod, (unsigned)sd_root, (unsigned)lf_contacts); + LittleFS.mkdir("/identity"); LittleFS.mkdir("/bl"); + const char *sub = sd_meshcomod >= sd_root ? "/meshcomod" : ""; // live layout root on the card + char sp2[64]; + migrateFsDir(SD_MMC, "/meshcomod/identity", LittleFS, "/identity", true); + migrateFsDir(SD_MMC, "/meshcomod/bl", LittleFS, "/bl", true); + static const char *k_files[] = { "/contacts3", "/channels2", "/adv_blobs", "/new_prefs", + "/new_prefs.tmp", "/regions2", "/ui_chat_history_v1.bin" }; + for (const char *nm : k_files) { + snprintf(sp2, sizeof sp2, "%s%s", sub, nm); + File probe = SD_MMC.open(sp2, FILE_READ); bool have = (bool)probe; if (probe) probe.close(); + if (!have && sub[0]) { snprintf(sp2, sizeof sp2, "%s", nm); } + else if (!have) { snprintf(sp2, sizeof sp2, "/meshcomod%s", nm); } + migrateFsCopyFile(SD_MMC, sp2, LittleFS, nm, true); + } + printf("[storage] #167 v4: done -- lfs used %u / %u KB, contacts3 readback %uB\n", + (unsigned)(LittleFS.usedBytes() / 1024), (unsigned)(LittleFS.totalBytes() / 1024), + (unsigned)fsize(LittleFS, "/contacts3")); + File w = LittleFS.open("/.hot_store_on_lfs_v4", FILE_WRITE); + if (w) { w.write((const uint8_t*)"1", 1); w.close(); } + } else { + printf("[storage] #167 v4: no migration (sd_id=%d sd=%uB lfs=%uB) -- LittleFS is the store\n", + (int)sd_has_id, (unsigned)sd_contacts, (unsigned)lf_contacts); + File w = LittleFS.open("/.hot_store_on_lfs_v4", FILE_WRITE); + if (w) { w.write((const uint8_t*)"1", 1); w.close(); } + } } - store.useSdMmcStorage(); + // Deliberately NOT calling store.useSdMmcStorage(): FFat stays the DataStore root. } store.begin(); diff --git a/variants/tdisplay_p4/RM69A10Display.cpp b/variants/tdisplay_p4/RM69A10Display.cpp index ee5d922..4cc8b35 100644 --- a/variants/tdisplay_p4/RM69A10Display.cpp +++ b/variants/tdisplay_p4/RM69A10Display.cpp @@ -10,6 +10,10 @@ #include "esp_lcd_panel_io.h" // esp_lcd_panel_io_tx_param (runtime brightness 0x51) #include "esp_lcd_panel_ops.h" #include "esp_lcd_rm69a10.h" +#if defined(TDP4_POKE_TRACE) +#include "soc/mipi_dsi_host_struct.h" // MIPI_DSI_HOST — raw host error status (poke-trace poller) +#include "freertos/task.h" +#endif #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" @@ -20,6 +24,22 @@ // the frame buffer as horizontal black/garbage bands, worst during transitions (many rapid // flushes). This binary semaphore makes each flush synchronous: draw_bitmap → wait for done. static SemaphoreHandle_t s_flush_sem = nullptr; +#if defined(TDP4_POKE_TRACE) +// #167 hunt, round 3: the esp_lcd driver watches ONLY the bridge-FIFO underrun (proven silent +// during visible flashes). The DSI HOST has its own never-read error latches: int_st0 = the +// panel acking packets WITH ERRORS, int_st1 = ECC/CRC/timeout/payload-write errors INCLUDING +// dpi_buff_pld_under (the host payload buffer underflowing -- a different FIFO than the bridge). +// Both are read-clear, so a 25 ms poll gives per-event capture. If these latch when the screen +// flashes, we have a hardware flash-detector and can bisect subsystems without eyes on the panel. +static void dsiErrPollTask(void*) { + for (;;) { + const uint32_t st0 = MIPI_DSI_HOST.int_st0.val; // RC + const uint32_t st1 = MIPI_DSI_HOST.int_st1.val; // RC + if (st0 || st1) printf("[DSIERR] %lu st0=%08X st1=%08X\n", (unsigned long)millis(), (unsigned)st0, (unsigned)st1); + vTaskDelay(pdMS_TO_TICKS(25)); + } +} +#endif static bool IRAM_ATTR rm69a10TransDone(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void*) { BaseType_t hpw = pdFALSE; if (s_flush_sem) xSemaphoreGiveFromISR(s_flush_sem, &hpw); @@ -38,7 +58,19 @@ static bool IRAM_ATTR rm69a10TransDone(esp_lcd_panel_handle_t, esp_lcd_dpi_panel #endif #define RM_DPI_MHZ 60 #define RM_LANES 2 -#define RM_BITRATE 1000 // Mbps per lane — TODO(device): confirm vs LilyGo RM69A10 define +// Lane bit rate. LilyGo/Meck ship 1000 Mbps (confirmed against both sources 2026-07-31), and the +// panel needs only ~550 Mbps of it at DPI 60 MHz / RGB565 / 2 lanes -- the rest is eye margin. +// #167 made that margin matter: SD-card write bursts load the P4's on-chip LDO4 and disturb the +// sibling LDO3 that powers the DSI PHY, and at a 1 ns unit interval that wobble was enough to +// glitch the link -- the panel loses a frame to a whole-screen blue flash (proven by elimination: +// no flash with the card out or unwritten; framebuffer/underrun/host-error/DCS all measured clean; +// identical timings+init to the flash-free Meck build). A 750 Mbps derate was tried against #167 and changed nothing +// (the disturbance reaches the panel around the link, not through it), so this stays at the +// vendor value. Overridable per build: -DRM_BITRATE_MBPS=. +#ifndef RM_BITRATE_MBPS +#define RM_BITRATE_MBPS 1000 +#endif +#define RM_BITRATE RM_BITRATE_MBPS #define RM_HSYNC 50 #define RM_HBP 150 #define RM_HFP 50 @@ -139,6 +171,9 @@ bool RM69A10Display::begin() { cbs.on_color_trans_done = rm69a10TransDone; esp_lcd_dpi_panel_register_event_callbacks(_panel, &cbs, nullptr); + #if defined(TDP4_POKE_TRACE) + xTaskCreatePinnedToCore(dsiErrPollTask, "dsierr", 3072, nullptr, 3, nullptr, 1); +#endif Serial.printf("[RM69A10] up %dx%d\n", RM_W, RM_H); return true; } @@ -221,10 +256,20 @@ void RM69A10Display::writePixelsRGB565(int x, int y, int w, int h, const uint16_ void RM69A10Display::setBrightness(uint8_t b) { if (!_dbi_io) return; +#if defined(TDP4_POKE_TRACE) + // #167 hunt: DCS 0x51 is a low-power command injected into the LIVE video stream -- if the + // panel glitches a frame per command, these lines will pace the visible flashes exactly. + printf("[BRI] %lu val=%u\n", (unsigned long)millis(), b); +#endif esp_lcd_panel_io_tx_param(_dbi_io, 0x51, &b, 1); // DCS SET_DISPLAY_BRIGHTNESS } +#if defined(TDP4_POKE_TRACE) +void RM69A10Display::turnOn() { printf("[DSP] %lu on\n", (unsigned long)millis()); if (_panel) esp_lcd_panel_disp_on_off(_panel, true); _on = true; } +void RM69A10Display::turnOff() { printf("[DSP] %lu off\n", (unsigned long)millis()); if (_panel) esp_lcd_panel_disp_on_off(_panel, false); _on = false; } +#else void RM69A10Display::turnOn() { if (_panel) esp_lcd_panel_disp_on_off(_panel, true); _on = true; } void RM69A10Display::turnOff() { if (_panel) esp_lcd_panel_disp_on_off(_panel, false); _on = false; } +#endif #endif // HAS_TDISPLAY_P4 diff --git a/variants/tdisplay_p4/Xl9535.cpp b/variants/tdisplay_p4/Xl9535.cpp index 6b4fa67..1c5a0cd 100644 --- a/variants/tdisplay_p4/Xl9535.cpp +++ b/variants/tdisplay_p4/Xl9535.cpp @@ -51,6 +51,11 @@ void Xl9535::write(uint8_t io, bool high) { uint8_t port, bit; split(io, port, bit); if (high) _out[port] |= (1 << bit); else _out[port] &= ~(1 << bit); +#if defined(TDP4_POKE_TRACE) + // #167 hunt: any expander write while the panel streams is a glitch suspect (this is an + // UNLOCKED shadow RMW -- a cross-task race can transiently drop SCREEN_RST or a rail bit). + printf("[XLW] %lu io=%u v=%d out=%02X%02X\n", (unsigned long)millis(), io, (int)high, _out[1], _out[0]); +#endif writeReg(REG_OUT0 + port, _out[port]); }