Files
wadamesh/tdisplay_p4/main/main.cpp
T

561 lines
30 KiB
C++

// wadamesh on the LilyGo T-Display P4 (AMOLED) — 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, 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 "esp_hosted.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "lvgl.h"
#include "c6_at.h" // AT-over-SDIO Wi-Fi/BLE driver for the on-board ESP32-C6 (P4-only)
#include <C6Socket.h> // C6Client — used directly by httpDateProbe() below
#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
// LAST include: rebind every WiFi.* below to the c6_at facade. Arduino's real WiFi object re-inits
// esp_hosted at mode()/begin() time and panics the P4 (the C6 runs ESP-AT, not an esp-hosted slave).
// With the facade, this file's Wi-Fi state machine drives REAL AT scans/joins instead.
#include <C6WifiShim.h>
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#ifndef WS_PORT
#define WS_PORT 8765
#endif
// --- C6 / esp-hosted bring-up gate -----------------------------------------------------------------
// The ESP32-C6 (Wi-Fi/BLE co-processor over SDIO) doesn't come up yet: the SDIO bus inits, but the
// C6's esp-hosted slave never signals ready, so esp_hosted_connect_to_slave() times out (~12 s) and
// esp-hosted resets the P4 — a boot loop that never reaches ui_task.begin(), leaving the AMOLED dark.
// Until the C6 reset/power/slave-firmware path is proven (needs a reset callback via the XL9535 — see
// TDISPLAY_P4_PORT.md), gate ALL C6-dependent bring-up OFF so the display + touch + LoRa come up.
// LoRa is a raw SX1262 on P4 GPIOs, independent of the C6, so the mesh still works over USB/LoRa.
// Flip to 1 once the C6 link is solid to restore Wi-Fi + BLE.
#ifndef TDP4_C6_READY
// C6 reset+enabled via the Meck-P4 C6_EN pulse (Xl9535::powerOnSequence) + host esp-hosted pinned to
// 2.0.17 to match the factory C6 slave protocol (see idf_component.yml). SDIO card inits AND the RPC
// channel should now sync, so connect to the C6 for Wi-Fi + BLE. (If the link ever misframes again the
// P4 reset-loops — set this back to 0 and re-flash the safe build.)
#define TDP4_C6_READY 0 // C6 connect OFF: factory C6 firmware misframes esp-hosted even at host 2.0.17 (see notes)
#endif
// 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.
#ifndef TDP4_BLE_READY
#define TDP4_BLE_READY 0
#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 void hostedConnectC6() {
esp_hosted_init(); // no-op if the constructor already ran
int rc = -1;
for (int i = 0; i < 4 && rc != 0; i++) {
rc = esp_hosted_connect_to_slave();
if (rc != 0) { printf("[BOOT] C6 connect try %d -> %d\n", i, rc); delay(250); }
}
printf("[BOOT] C6 hosted link: %s\n", rc == 0 ? "UP" : "FAILED");
}
// 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: runs the FACTORY ESP-AT firmware (never esp-hosted, never reflashed — Kaj's product
// decision 2026-07-15). Wi-Fi comes up via the c6_at AT-over-SDIO worker spawned at the end of
// setup; BLE companion is unavailable on this AT build (advertising commands stubbed) so the
// phone pairs over Wi-Fi (TCP:5000) or USB.
#if TDP4_C6_READY
hostedConnectC6(); // never taken (TDP4_C6_READY=0): kept only as a reference to the hosted path
#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; 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);
serial_interface.begin(Serial, TCP_PORT, WS_PORT);
serial_interface.setBroadcastResponses(true);
the_mesh.startInterface(serial_interface);
#if defined(BLE_PIN_CODE) && TDP4_C6_READY && TDP4_BLE_READY
if (hostedInitBLE()) {
char* nm = the_mesh.getNodePrefs()->node_name;
serial_interface.prepareBle("wadamesh-", nm, the_mesh.getBLEPin());
if (wifiConfigGetBleEnabled())
serial_interface.beginBle("wadamesh-", nm, the_mesh.getBLEPin());
} else {
printf("[BOOT] hostedInitBLE FAILED\n");
}
#endif
// 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 L76K repeats the full set about once a second).
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);
printf("[GPS] UART probe: %lu bytes, %lu lines in 1.2s @ %d baud, module TX -> GPIO%d\n",
(unsigned long)bytes, (unsigned long)lines, (int)GPS_BAUD_RATE, (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 AT/SDIO Wi-Fi: spawn the c6_at worker (non-blocking — it brings the AT link up itself on
// core 1 with retries). The Wi-Fi state machine in app_main's loop then drives scans/joins through
// the C6WifiShim facade exactly like the other boards drive Arduino WiFi.
c6at_worker_start();
}
// 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) {
C6Client c;
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) {
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.)
const bool c6_up = c6at_is_up();
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
uint32_t e = c6at_sntp_epoch();
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);
}
}