Files
wadamesh/tdisplay_p4/main/main.cpp
T
Kaj SchittecatandClaude Opus 5 b52c71f4a5 p4: stop retrying a C6 that is never coming back
7ce1b3f turned off CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE so a wrong-
firmware C6 could not reboot the board. It stopped the boot loop and I called
it fixed on a 45 s capture. That was the wrong test. Disabling the restart did
not stop esp-hosted, it only removed its exit: sdio_read_task posts
ESP_HOSTED_EVENT_TRANSPORT_FAILURE and then `continue`s straight back into a
register read that is going to fail again. Measured on the same board:

  731 failed register reads in 45 s, ~2.2 KB/s of log, forever

A boot loop and an unbounded retry storm are the same mistake: an unreachable
co-processor is a PERMANENT condition and the driver treats it as transient.

* The rx task now parks after the failure event is posted (build-time patch to
  the esp-hosted fork, same idempotent mechanism build.sh already uses for
  libsodium and the SD_MMC pull-ups; it fails loudly if upstream moves that
  code rather than silently not applying). No reboot, no retry: Wi-Fi and BLE
  are unavailable and everything else carries on.

* hostedInitBLE() no longer runs unconditionally. It is a full esp-hosted
  handshake and it ran on every boot even though the banner advertises lazy
  initialisation -- so on a mismatched unit it started a transport that can
  never come up, for a radio the user may have had switched off. It now
  honours the same stored intent beginBle() already checked.

* The keyboard hot-plug probe backs off from 1.5 s to 15 s once the expansion
  has been absent a few times, and returns to the fast rate the moment one
  succeeds. The probe is a real I2C transaction and on an empty bus Arduino's
  Wire layer logs two [E] lines per attempt: a permanent drip down the USB-CDC
  the companion app talks over, for an accessory most people do not own.

Measured on an ESP-AT unit running the HOSTED image, 150 s captures:

                      shipped   7ce1b3f only   this
  resets                 5           0           0
  host restarts         yes         yes          0
  failed reg reads       -        731/45s        1
  steady log rate        -        2.2 KB/s    ~14 B/s
  UI                   never      appears     stays up

The remaining ~14 B/s is the backed-off keyboard probe on a board with no
expansion attached, which is the expected floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 10:26:17 +02:00

621 lines
33 KiB
C++

// wadamesh on the LilyGo T-Display P4 — the app entry point.
//
// Standalone ESP-IDF app (NOT an AppFS/launcher app like the Tanmatsu): it owns the USB console,
// drives the XL9535 expander to power the board, brings up the RM69A10 AMOLED + the C6 (esp-hosted)
// + the raw SX1262 or LR2021, then runs the shared UITask. The board/radio/display globals + radio_init() live
// in variants/tdisplay_p4/target.cpp (via target.h). Modeled on tanmatsu/main/main.cpp; the WiFi +
// companion loop is ported faithfully. Bring-up TODOs (touch, brightness, DSI/power tuning) in
// variants/tdisplay_p4/TDISPLAY_P4_PORT.md.
#include <tdisplay_p4_compat.h> // adcAttachPin() shim — BEFORE target.h pulls ESP32Board.h
#include <Arduino.h>
#include <Mesh.h>
#include "MyMesh.h"
#include <new> // placement-new for the PSRAM-resident the_mesh
#include "esp_heap_caps.h"
#include "UITask.h"
#include "target.h" // board, radio_driver, rtc_clock, display, sensors, radio_init()
#include <LittleFS.h> // 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 <SD_MMC.h> // microSD on SDMMC slot 0 (primary store)
#include "esp_partition.h"
#include <WiFi.h>
#include "../../src/helpers/esp32/MultiTransportCompanionInterface.h"
#include <helpers/esp32/WifiRuntimeStore.h> // wifiConfig* (runtime Wi-Fi state the loop drives)
#include <helpers/esp32/TouchPrefsStore.h> // touchPrefsBuildLocalTz + WIFI_CONFIG_* sizes
#include "../../src/helpers/esp32/BootTimeSync.h" // opt-in cold-boot clock sync over saved Wi-Fi (#383)
#include "esp_hosted.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "lvgl.h"
#if !TDP4_C6_HOSTED
#include "c6_at.h" // legacy AT-over-SDIO backend
#include <C6Socket.h>
#endif
#include "esp_vfs_fat.h" // native SD probe (esp_vfs_fat_sdmmc_mount)
#include "driver/sdmmc_host.h" // SDMMC_HOST_DEFAULT / sdmmc_slot_config_t for the probe
#include "sdmmc_cmd.h"
#include "sd_pwr_ctrl_by_on_chip_ldo.h" // SD IO rail = P4 on-chip LDO channel 4
// Legacy builds rebind WiFi.* to the ESP-AT facade. Current units ship hosted
// C6 firmware and use Arduino's standard remote Wi-Fi implementation.
#if !TDP4_C6_HOSTED
#include <C6WifiShim.h>
#endif
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#ifndef WS_PORT
#define WS_PORT 8765
#endif
// --- C6 / esp-hosted bring-up gate -----------------------------------------------------------------
// TWO HARDWARE GENERATIONS, and the image has to match the one in front of it. Current T-Display P4
// V1 units ship the C6 with esp-hosted firmware (2.12.3) and are what TDP4_C6_HOSTED=1 (the default)
// is for. OLDER units still carry the factory ESP-AT firmware and need WADA_P4_LEGACY_AT=1. Flash a
// hosted image onto an ESP-AT unit and the SDIO stream never frames — the log fills with
// "H_SDIO_DRV: len[...]>max[1524] ... Drop" and Wi-Fi/BLE simply never arrive.
//
// That mismatch must cost Wi-Fi and BLE and NOTHING ELSE. All C6-dependent bring-up therefore runs
// at the END of setup, after ui_task.begin() — see the block there. LoRa is a raw SX1262/LR2021 on
// P4 GPIOs, independent of the C6, so the mesh still works over USB/LoRa on a mismatched unit.
//
// esp-hosted RESETS THE HOST on a transport failure, which is what turns a mismatch into a boot
// loop rather than a missing feature. There are two reset paths and they need separate switches:
// CONFIG_ESP_HOSTED_HOST_RESTART_NO_COMMUNICATION_WITH_SLAVE=n covers one, and
// CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE=n covers the one that actually fires here, about
// 10 s after the connect attempt begins (sdio_drv.c, H_TRANSPORT_RESTART_ON_FAILURE). Both are off
// in sdkconfigs/tdisplay_p4. Measured on an ESP-AT unit before the second one was set: five resets
// in a 45 s capture, each rst:0x9 HP_SYS_LP_WDT_RESET. Do not re-enable either.
#define TDP4_C6_READY TDP4_C6_HOSTED
// BLE over the C6 uses arduino-esp32's hostedInitBLE(), which only exists in arduino-esp32 >=3.3.10.
// We pin 3.3.0 (so esp_hosted can be the C6-compatible 2.0.17), which predates hostedInitBLE — so BLE
// needs its own bring-up. Keep BLE gated OFF until that's provided; Wi-Fi (esp_wifi_remote) works now.
#define TDP4_BLE_READY TDP4_C6_HOSTED
#if TDP4_C6_HOSTED && defined(CONFIG_BT_NIMBLE_TRANSPORT_UART)
#error "T-Display P4 hosted C6 requires NimBLE hosted HCI; disable CONFIG_BT_NIMBLE_TRANSPORT_UART"
#endif
extern "C" bool hostedInitBLE(); // arduino-esp32 BLE controller bring-up over esp-hosted
// Boot-trace hooks the shared code defines on the S3 boards (src/main.cpp, excluded here).
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(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;
SimpleMeshTables tables;
UITask ui_task(&board, &serial_interface);
// the_mesh (~42 KB, MAX_CONTACTS-dominated) lives in the 32 MB PSRAM, not scarce internal DRAM.
static MyMesh& makeTheMesh() {
void* mem = heap_caps_malloc(sizeof(MyMesh), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!mem) mem = malloc(sizeof(MyMesh));
return *new (mem) MyMesh(radio_driver, fast_rng, rtc_clock, tables, store, &ui_task);
}
MyMesh& the_mesh = makeTheMesh();
static constexpr bool s_hosted_c6_up = true; // hostedInit() connects lazily on first Wi-Fi/BLE use
// 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, 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);
if (!out) { in.close(); return; }
static uint8_t buf[1024];
int n;
while ((n = in.read(buf, sizeof buf)) > 0) out.write(buf, (size_t)n);
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, bool overwrite = false) {
File d = src.open(sdir);
if (!d || !d.isDirectory()) { if (d) d.close(); return; }
dst.mkdir(ddir);
File f;
while ((f = d.openNextFile())) {
const char *name = f.name();
const char *leaf = strrchr(name, '/');
leaf = leaf ? leaf + 1 : name;
char sp[128], dp[128];
snprintf(sp, sizeof sp, "%s/%s", strcmp(sdir, "/") == 0 ? "" : sdir, leaf);
snprintf(dp, sizeof dp, "%s/%s", ddir, leaf);
bool is_dir = f.isDirectory();
f.close();
if (is_dir) migrateFsDir(src, sp, dst, dp, overwrite);
else migrateFsCopyFile(src, sp, dst, dp, overwrite);
}
d.close();
}
static void wadameshSetup() {
Serial.begin(115200);
delay(150);
printf("[BOOT] wadamesh / T-Display P4\n");
// 1. XL9535 expander FIRST — it powers the rails and enables the C6, SD, screen + touch.
if (!xl9535.begin(7, 8)) printf("[BOOT] XL9535 init FAILED (board will not power up)\n");
xl9535.powerOnSequence();
// 2. AMOLED (RM69A10 MIPI-DSI) — up before LVGL flushes to it.
if (!display.begin()) printf("[BOOT] RM69A10 display init FAILED\n");
// 3. lwIP/tcpip + default event loop before any WiFi/netif call (esp_wifi_remote needs it).
esp_netif_init();
esp_event_loop_create_default();
// 4. C6: which firmware it runs depends on the unit's age — see the generation note at the top.
// Either way nothing happens HERE: both backends are started at the end of setup, after the
// UI. On the ESP-AT build the c6_at AT-over-SDIO worker carries Wi-Fi and the BLE companion is
// unavailable (advertising commands stubbed), so the phone pairs over Wi-Fi (TCP:5000) or USB.
#if TDP4_C6_HOSTED
printf("[BOOT] C6 = esp-hosted (lazy Wi-Fi/BLE initialization)\n");
#else
printf("[BOOT] C6 = factory ESP-AT (Wi-Fi via c6_at worker; BLE companion unavailable on this build)\n");
#endif
// 5. Radio (raw SX1262/LR2021; reset via the XL9535 inside radio_init).
if (!radio_init()) printf("[BOOT] radio_init FAILED\n");
DisplayDriver* disp = &display;
// 6. Storage — microSD (SDMMC slot 0) primary, internal FFat 'storage' as the no-card fallback.
// 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,
// card powered. Our Xl9535::begin() had parked it output-HIGH.) Card supply also wants the P4's
// on-chip LDO4 attached like Meck does — Arduino's SD_MMC defaults to exactly that on this chip.
// Pins are the board's SDMMC slot 0 IOMUX set (CLK43/CMD44/D0-3=39..42), 4-bit like Meck.
SD_MMC.setPins(43, 44, 39, 40, 41, 42);
// Meck's working init also sets SDMMC_SLOT_FLAG_INTERNAL_PULLUP (build.sh patches Arduino's slot-0
// struct literal to add it). The powered board turns out to have external pull-ups on all six
// lines too — belt and braces.
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[] = {
{ 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" },
};
sdMountDiagBegin();
uint32_t mounted_hz = 0;
for (auto &t : tries) {
const bool begin_ok = SD_MMC.begin("/sdcard", t.onebit, false, t.khz);
g_sd_ok = begin_ok && SD_MMC.cardType() != CARD_NONE;
sdMountDiagAttempt((uint32_t)t.khz * 1000u, begin_ok, g_sd_ok);
printf("[storage] SD_MMC try %s -> %s\n", t.tag, g_sd_ok ? "OK" : "fail");
if (g_sd_ok) { mounted_hz = (uint32_t)t.khz * 1000u; break; }
SD_MMC.end();
delay(120);
}
sdMountDiagSetMounted(g_sd_ok, mounted_hz);
}
printf("[storage] SD_MMC = %s\n", g_sd_ok ? "OK" : "no card");
if (g_sd_ok) {
// #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(); }
}
}
#if defined(TDP4_SD_STORE_TEST)
// #167 EXPERIMENT: store back on the SD card with all hot writes hopped to core 0 (the tile
// task's core, which has never flashed). One variable vs the known-flashing config: the
// executing core. Uses the card's existing (slightly stale) store; migration skipped.
printf("[storage] TDP4_SD_STORE_TEST: store on SD, writes on core 0\n");
store.useSdMmcStorage();
#else
// Deliberately NOT calling store.useSdMmcStorage(): the internal store stays the DataStore root.
#endif
}
store.begin();
the_mesh.begin(disp != NULL);
#if TDP4_C6_HOSTED
wifiConfigBegin(); // NVS only, no radio — safe this early, and the getters assume it
#endif
serial_interface.begin(Serial, TCP_PORT, WS_PORT);
serial_interface.setBroadcastResponses(true);
the_mesh.startInterface(serial_interface);
// GPS UART resilience (same fix as the S3 boards): the core opens Serial1 with Arduino's
// 256-byte RX ring; one long LVGL frame overflows it and corrupts NMEA, stretching TTFF from
// ~1 min to many. Must precede sensors.begin() (a no-op once the UART runs). Unconditional
// here — the P4 has RAM to spare, and GPS detection itself needs an intact first second.
Serial1.setRxBufferSize(4096);
sensors.begin();
{ // ---- GPS reality check -------------------------------------------------------------
// The line that used to be here reported whether the "gps" SETTING got registered, which on
// this board is FORCED true by ENV_SKIP_GPS_DETECT (we set that because the P4's heavy boot
// misses the core's 1 s detect window, and a false negative hides the GPS toggle entirely).
// So it printed DETECTED unconditionally and proved nothing, including across several rounds
// of "GPS still isn't working". Read the UART directly instead and report what arrives.
bool has_setting = false;
for (int i = 0; i < sensors.getNumSettings(); i++)
if (strcmp(sensors.getSettingName(i), "gps") == 0) has_setting = true;
// Drain a SHORT window of NMEA. Bytes at all => pins, rails, wake and baud are ALL correct and
// a stuck "acquiring" is a FIX problem (antenna / sky view). Zero bytes => the link is dead.
// 1.2 s, not longer: this runs on every boot and it CONSUMES bytes the NMEA parser would
// otherwise see, so it is deliberately just long enough to catch one of each sentence type
// (the P4's factory-configured L76K emits its enabled set at 5 Hz).
uint32_t bytes = 0, lines = 0;
char first[96]; first[0] = '\0';
char cur[96]; size_t cl = 0;
// Keep one example of each distinct GSV/GGA/RMC talker so every constellation is represented
// without dumping the whole stream (the same 5 sentence types repeat every second).
char kept[10][96]; int kept_n = 0;
const uint32_t until = millis() + 1200;
while ((int32_t)(millis() - until) < 0) {
while (Serial1.available() > 0) {
const int c = Serial1.read();
if (c < 0) break;
++bytes;
if (c == '\n' || c == '\r') {
if (cl) {
cur[cl] = '\0';
++lines;
if (!first[0] && cur[0] == '$') { strncpy(first, cur, sizeof first - 1); first[sizeof first - 1] = '\0'; }
// Keep the first of each sentence TYPE (talker + type, e.g. "$GPGSV", "$GNGGA").
if (cur[0] == '$' && cl >= 6 && kept_n < 10) {
bool seen = false;
for (int k = 0; k < kept_n; ++k) if (strncmp(kept[k], cur, 6) == 0) { seen = true; break; }
if (!seen) { strncpy(kept[kept_n], cur, sizeof kept[0] - 1); kept[kept_n][sizeof kept[0] - 1] = '\0'; ++kept_n; }
}
cl = 0;
}
} else if (cl < sizeof(cur) - 1) {
cur[cl++] = (char)c;
}
}
delay(5);
}
printf("[GPS] setting_registered=%d (forced by ENV_SKIP_GPS_DETECT, proves nothing)\n", (int)has_setting);
const uint32_t active_baud = touchPrefsGetGpsBaud(GPS_BAUD_RATE);
printf("[GPS] UART probe: %lu bytes, %lu lines in 1.2s @ %lu baud, module TX -> GPIO%d\n",
(unsigned long)bytes, (unsigned long)lines, (unsigned long)active_baud, (int)PIN_GPS_TX);
if (first[0]) printf("[GPS] first sentence: %s\n", first);
else if (bytes) printf("[GPS] bytes but no complete '$' sentence -> wrong baud or line noise\n");
else printf("[GPS] NOTHING on the UART -> check wake (XL9535 IO11 HIGH), rails, RX pin\n");
// Satellite visibility across ALL constellations, plus the fix line. One GxGSV is not enough:
// GLGSV is GLONASS only, so "00" there says nothing about GPS/Galileo/BeiDou. GSV field 3 is
// sats-in-view for that constellation; GGA field 6 is fix quality (0 = no fix) and field 7 the
// sats used. Non-zero in-view with quality 0 means it IS hearing satellites and just needs
// time or better sky. All-zero in-view points at the antenna or shielding.
if (kept_n) {
printf("[GPS] --- satellite view (%d sentences) ---\n", kept_n);
for (int i = 0; i < kept_n; ++i) printf("[GPS] %s\n", kept[i]);
}
}
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs());
board.onBootComplete();
printf("[BOOT] setup done\n");
#ifdef TDP4_C6_FLASH_HELPER
// C6-FLASH HELPER BUILD: the P4 must NOT touch the C6 so it can be held in ROM download mode and
// reflashed over its own USB. powerOnSequence() already skips the C6_EN reset pulse under this flag
// (leaving C6_EN HIGH = powered), and here we skip the AT worker entirely. Boots the normal UI so
// the board is alive, but the C6 is left completely free. Temporary — remove after the C6 reflash.
printf("[BOOT] C6-FLASH HELPER: AT worker NOT started, C6 left free for download-mode reflash\n");
board.onBootComplete();
printf("[BOOT] setup done (helper)\n");
return;
#endif
// ---- C6 bring-up: AFTER the UI, for BOTH backends --------------------------------------------
// Nothing below here may run before ui_task.begin(). The C6 is a separate chip that can be
// unreachable — an older unit still carries the factory ESP-AT firmware while this build speaks
// esp-hosted, and then the SDIO stream never frames (H_SDIO_DRV drop storm). The legacy backend
// always started its worker here, after the UI, so a dead C6 cost you Wi-Fi and nothing else.
// The hosted backend was wired in ahead of the UI instead, so the same dead C6 held the AMOLED
// dark through a handshake that could not succeed, and the board read as bricked. Both now start
// here. A co-processor must never be able to take the display, touch and LoRa down with it.
#if !TDP4_C6_HOSTED
// Legacy factory ESP-AT units use the asynchronous AT worker.
c6at_worker_start();
#else
#if defined(BLE_PIN_CODE) && TDP4_C6_READY && TDP4_BLE_READY
// Only bring the C6 up for BLE when BLE is actually wanted. hostedInitBLE()
// is a full esp-hosted handshake, and it ran unconditionally here even though
// the boot banner says "lazy Wi-Fi/BLE initialization" -- so every boot paid
// for it, and on a unit whose C6 speaks ESP-AT it started a transport that
// can never come up for a radio the user had switched off. Honour the stored
// intent the way beginBle() below already did.
if (wifiConfigGetBleEnabled()) {
if (hostedInitBLE()) {
char* nm = the_mesh.getNodePrefs()->node_name;
serial_interface.prepareBle("wadamesh-", nm, the_mesh.getBLEPin());
serial_interface.beginBle("wadamesh-", nm, the_mesh.getBLEPin());
} else {
printf("[BOOT] hostedInitBLE FAILED\n");
}
} else {
printf("[BOOT] BLE off in settings: C6 not started for it\n");
}
#endif
// Cold-boot clock sync over SAVED Wi-Fi (#383, BootTimeSync.h) -- the same opt-in
// Settings > Clock switch the T-Deck/M9 have. Returns Skipped at no cost unless this was a
// true power-on, the user turned it on, and the PCF8563 did not already give a trustworthy
// time. It used to sit before the transports so its temporary Wi-Fi session had nothing live
// to disturb; from here the transports exist but none of them is associated yet, and the UI
// being up first is worth more than that ordering nicety. The clock simply corrects itself a
// moment after the screen appears instead of the screen waiting on the network.
{
uint32_t synced_epoch = 0;
const BootTimeSyncResult r = bootTimeSyncRun(rtc_clock.timeIsCurrent(), synced_epoch);
if (r == BootTimeSyncResult::Ok) {
rtc_clock.setCurrentTime(synced_epoch); // floor + system clock + RTC chip, like an NTP sync
printf("[BOOT] cold-boot time sync ok: %lu\n", (unsigned long)synced_epoch);
} else if (r != BootTimeSyncResult::Skipped) {
printf("[BOOT] cold-boot time sync: %s\n", bootTimeSyncResultName(r));
}
}
#endif
}
// Time-sync fallback: pull UTC from the Date header of a plain-HTTP HEAD to the firmware
// host. C6-SNTP needs UDP/123 egress, which some networks (guest/IoT VLANs) block — while
// port-80 HTTP is proven open on this device (tiles + version checks ride it). Returns a
// UTC epoch, or 0. Runs on the loop thread only while the clock is still unsynced.
static uint32_t httpDateProbe(void) {
#if TDP4_C6_HOSTED
WiFiClient c;
#else
C6Client c;
#endif
if (!c.connect("firmware.wadamesh.com", 80, 8000)) return 0;
static const char req[] =
"HEAD / HTTP/1.1\r\nHost: firmware.wadamesh.com\r\nConnection: close\r\n\r\n";
if (c.write((const uint8_t*)req, sizeof(req) - 1) != sizeof(req) - 1) { c.stop(); return 0; }
char buf[600];
size_t got = 0;
uint32_t t0 = millis();
while (got < sizeof(buf) - 1 && millis() - t0 < 6000) {
int n = c.read((uint8_t*)buf + got, sizeof(buf) - 1 - got);
if (n > 0) { got += (size_t)n; buf[got] = '\0'; if (strstr(buf, "\r\n\r\n")) break; }
else if (!c.connected()) break;
else vTaskDelay(pdMS_TO_TICKS(20));
}
c.stop();
buf[got] = '\0';
const char* p = strstr(buf, "\r\nDate: "); // "Date: Tue, 15 Jul 2026 16:33:47 GMT"
if (!p) return 0;
char mon[4] = {0};
int day, year, hh, mm, ss;
if (sscanf(p + 8, "%*3s, %d %3s %d %d:%d:%d", &day, mon, &year, &hh, &mm, &ss) != 6) return 0;
if (year < 2024) return 0;
static const char* M = "JanFebMarAprMayJunJulAugSepOctNovDec";
const char* mp = strstr(M, mon);
if (!mp) return 0;
int m = (int)(mp - M) / 3 + 1;
int y = year - (m <= 2); // days-from-civil (Howard Hinnant)
int era = y / 400, yoe = y - era * 400;
int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + day - 1;
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
long days = (long)era * 146097 + doe - 719468;
return (uint32_t)(days * 86400L + hh * 3600 + mm * 60 + ss);
}
extern "C" void app_main(void) {
// Before anything else registers a shutdown handler: a software restart must be a full system
// reset or the panel comes back dark (see target.cpp).
tdisplayP4InstallFullRestart();
initArduino();
wadameshSetup();
// WiFi state machine + SNTP + TCP/WS companion server — ported from src/main.cpp's loop()
// (excluded from this build), identical to the Tanmatsu.
bool wifi_started = false, wifi_radio_prev = true, wifi_radio_inited = false;
bool sntp_kicked = false, sntp_pushed = false, modem_sleep_set = false;
uint32_t last_wifi_retry_ms = 0, sntp_kick_ms = 0;
const uint32_t WIFI_RETRY_INTERVAL_MS = 10000;
for (;;) {
ui_task.loop();
// WiFi.* here is the C6WifiShim facade (AT-over-SDIO), so this state machine drives REAL joins:
// start once the c6_at worker has the AT link up + the user wants Wi-Fi. The 10 s retry branch
// doubles as auto-reconnect. (TDP4_C6_READY still gates the unused esp-hosted path elsewhere.)
#if TDP4_C6_HOSTED
const bool c6_up = s_hosted_c6_up;
#else
const bool c6_up = c6at_is_up();
#endif
bool wifi_radio_en = c6_up && wifiConfigWantsWifi();
// The C6 runs its own AT firmware with auto-connect enabled, so after a power
// cycle it rejoins the last AP by itself, before this loop ever asks it to.
// So latch only once the AT link is actually up, and APPLY the user's choice
// at that moment rather than merely recording it: with the radio pref off
// there is no on->off transition for the branch below to catch, the join the
// C6 made on its own is never torn down, and the device comes up connected
// while every switch in the UI reads off (#373).
if (!wifi_radio_inited) {
if (c6_up) {
wifi_radio_inited = true;
wifi_radio_prev = wifi_radio_en;
if (!wifi_radio_en) { WiFi.disconnect(true); delay(50); WiFi.mode(WIFI_OFF); }
}
}
else if (wifi_radio_en != wifi_radio_prev) {
wifi_radio_prev = wifi_radio_en;
if (!wifi_radio_en) { WiFi.disconnect(true); delay(50); WiFi.mode(WIFI_OFF); }
wifi_started = false;
}
if (wifiConfigConsumeApplyRequest()) {
if (wifi_started) {
if (!wifi_radio_en) { WiFi.disconnect(true); delay(50); WiFi.mode(WIFI_OFF); }
else { WiFi.disconnect(false, false); delay(50); }
}
wifi_started = false; last_wifi_retry_ms = 0;
}
if (wifi_radio_en) {
if (!wifi_started) {
wifi_started = true;
WiFi.mode(WIFI_STA);
if (wifiConfigHasRuntime()) {
char ssid[WIFI_CONFIG_SSID_MAX], pwd[WIFI_CONFIG_PWD_MAX];
wifiConfigGetSsid(ssid, sizeof(ssid)); wifiConfigGetPwd(pwd, sizeof(pwd));
if (strlen(ssid) > 0) { WiFi.begin(ssid, pwd[0] ? pwd : nullptr); last_wifi_retry_ms = millis(); }
}
}
if (wifiConfigHasRuntime() && WiFi.status() != WL_CONNECTED) {
uint32_t now = millis();
if ((uint32_t)(now - last_wifi_retry_ms) >= WIFI_RETRY_INTERVAL_MS) {
last_wifi_retry_ms = now;
char ssid[WIFI_CONFIG_SSID_MAX], pwd[WIFI_CONFIG_PWD_MAX];
wifiConfigGetSsid(ssid, sizeof(ssid)); wifiConfigGetPwd(pwd, sizeof(pwd));
if (strlen(ssid) > 0) { WiFi.disconnect(false, true); WiFi.begin(ssid, pwd[0] ? pwd : nullptr); }
}
}
if (WiFi.status() == WL_CONNECTED) {
if (!modem_sleep_set) { WiFi.setSleep(true); modem_sleep_set = true; }
serial_interface.startTcpServer(true);
// NTP runs ON the C6 (AT+CIPSNTP*, configured by the c6_at worker after a join) — lwIP has no
// netif on this board, so esp_sntp/configTzTime can't sync. Pull the UTC epoch when ready.
if (!sntp_pushed) {
if (sntp_kick_ms == 0) sntp_kick_ms = millis(); // connected-since stamp for the fallback below
#if TDP4_C6_HOSTED
if (!sntp_kicked) {
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
sntp_kicked = true;
}
uint32_t e = (uint32_t)time(nullptr);
#else
uint32_t e = c6at_sntp_epoch();
#endif
if (e > 1700000000) { rtc_clock.setCurrentTime(e); sntp_pushed = true; printf("[C6-AT] SNTP -> rtc %lu\n", (unsigned long)e); }
// Bench-verified 2026-07-15: on some networks SNTP never completes (UDP/123
// blocked) — the clock then stays wrong forever, since BLE is parked and the
// phone app only sets time forward. After 45 s connected without a sync, fall
// back to HTTP Date (sets the RTC through the same guarded ClockFloorRTC path,
// so it also *repairs* a garbage-future hardware clock via the backward hatch).
else if (millis() - sntp_kick_ms > 45000) {
static uint32_t http_date_next = 0;
if ((int32_t)(millis() - http_date_next) >= 0) {
http_date_next = millis() + 120000;
uint32_t he = httpDateProbe();
if (he > 1700000000) {
rtc_clock.setCurrentTime(he);
sntp_pushed = true;
printf("[TIME] HTTP-Date -> rtc %lu\n", (unsigned long)he);
}
}
}
}
(void)sntp_kicked;
}
}
serial_interface.tickWebSocketHandshake();
// Service the sensor manager. This was MISSING on this board: sensors.begin() ran at boot and
// then nothing ever pumped it again, while every S3 board calls this each iteration (see
// src/main.cpp, which is excluded from this build — that is how the call got lost in the port).
//
// sensors.loop() is what drains the GPS UART into MicroNMEA. Without it the parser never sees a
// byte, so isValid() stays false forever and the UI sits on "searching" no matter how good the
// sky is — while the module itself is perfectly happy and does acquire a fix (proven with a raw
// UART probe: a valid 3D fix at HDOP 2.2 that the firmware never knew about). It also starves
// everything else the manager provides: telemetry sensors and the GPS->RTC time sync.
sensors.loop();
the_mesh.loop();
vTaskDelay(1);
}
}