From b52c71f4a570e17e93bcc30ea561512716cdbc5a Mon Sep 17 00:00:00 2001 From: Kaj Schittecat Date: Mon, 5 Oct 2026 10:26:17 +0200 Subject: [PATCH] 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 --- tdisplay_p4/build.sh | 47 +++++++++++++++++++++++++++++ tdisplay_p4/main/main.cpp | 19 +++++++++--- variants/tdisplay_p4/P4Keyboard.cpp | 15 ++++++++- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/tdisplay_p4/build.sh b/tdisplay_p4/build.sh index e23f877..c635297 100755 --- a/tdisplay_p4/build.sh +++ b/tdisplay_p4/build.sh @@ -107,6 +107,53 @@ if [ -f "$SDMMC_C" ] && ! grep -q 'wadamesh P4: internal pullups' "$SDMMC_C"; th sed -i '' 's|sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();|sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();\n slot_config.flags \|= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; // wadamesh P4: internal pullups (no external ones on the SD lines)|' "$SDMMC_C" echo "[build.sh] patched arduino SD_MMC.cpp (internal pull-ups on the SD slot)" fi +# --- Build-time patch: a dead C6 must not spin the SDIO rx task ----------------------------------- +# With CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE=n (see sdkconfigs/tdisplay_p4 for why it is +# off), 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. On a unit whose C6 runs factory ESP-AT the +# transport NEVER comes up, so that is an unbounded retry storm: measured at ~16 failed reads per +# second, forever, with the whole transport re-initialising on top of it. It floods the USB-CDC the +# companion app talks over and keeps a task busy for the life of the session. +# +# Turning the restart back on is not the answer — that is the boot loop this board had before. +# Both are the same mistake: a co-processor that cannot be reached is a permanent condition, and +# the driver treats it as transient. Park the task instead. The failure event has already been +# posted, Wi-Fi and BLE are simply unavailable, and everything else on the device keeps running. +# Idempotent. +HOSTED_SDIO="managed_components/nicolaielectronics__esp-hosted-tanmatsu/host/drivers/transport/sdio/sdio_drv.c" +if [ -f "$HOSTED_SDIO" ] && ! grep -q 'wadamesh: park instead of spinning' "$HOSTED_SDIO"; then + python3 - "$HOSTED_SDIO" <<'PYEOF' +import re, sys +p = sys.argv[1] +s = open(p).read() +# Both failure arms look identical apart from the log line: event post, optional restart, continue. +old = """#if H_TRANSPORT_RESTART_ON_FAILURE + ESP_LOGI(TAG, "Host is resetting itself, to avoid any sdio race condition"); + g_h.funcs->_h_restart_host(); +#endif + continue;""" +new = """#if H_TRANSPORT_RESTART_ON_FAILURE + ESP_LOGI(TAG, "Host is resetting itself, to avoid any sdio race condition"); + g_h.funcs->_h_restart_host(); +#else + /* wadamesh: park instead of spinning. The transport is down and we are + * not allowed to reboot the host, so retrying can only fail again at + * full speed. The failure event is already posted; stop here and let the + * rest of the device run without Wi-Fi/BLE. */ + ESP_LOGE(TAG, "transport down, host restart disabled: parking rx task (Wi-Fi/BLE unavailable)"); + while (1) g_h.funcs->_h_msleep(60000); +#endif + continue;""" +n = s.count(old) +if n == 0: + sys.exit("esp-hosted sdio_drv.c: failure arm not found; patch needs updating") +s = s.replace(old, new) +open(p, "w").write(s) +print(" patched %d failure arm(s)" % n) +PYEOF + echo "[build.sh] patched esp-hosted sdio_drv.c (park the rx task on a dead transport)" +fi + # ⚠️ The P4/slot-0 branch REBUILDS slot_config from scratch with `.flags = 0`, silently discarding # the flag added above (that's the branch that actually runs with BOARD_SDMMC_SLOT=0) — so patch that # struct literal too. Without the flag the IDF host explicitly FLOATS the pads -> deterministic 0x109 diff --git a/tdisplay_p4/main/main.cpp b/tdisplay_p4/main/main.cpp index bf949d2..e5c92e7 100644 --- a/tdisplay_p4/main/main.cpp +++ b/tdisplay_p4/main/main.cpp @@ -411,13 +411,22 @@ static void wadameshSetup() { c6at_worker_start(); #else #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()) + // 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] hostedInitBLE FAILED\n"); + 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 diff --git a/variants/tdisplay_p4/P4Keyboard.cpp b/variants/tdisplay_p4/P4Keyboard.cpp index 96ef4a3..d4e2378 100644 --- a/variants/tdisplay_p4/P4Keyboard.cpp +++ b/variants/tdisplay_p4/P4Keyboard.cpp @@ -35,6 +35,9 @@ namespace { constexpr uint32_t kHotplugMs = 1500; // how often the expansion is looked for static bool s_scanned = false; // one-shot bus scan (see p4KeyboardPoll) +constexpr uint32_t kHotplugIdleMs = 15000; // probe rate once the expansion looks absent +constexpr uint8_t kMissesBeforeBackoff = 4; +static uint8_t s_misses = 0; // consecutive probes that found nothing constexpr uint32_t kIdlePollMs = 120; // backstop drain when INT says nothing is waiting constexpr uint32_t kModTimeoutMs = 1500; // a one-shot modifier lapses after this constexpr uint32_t kRepeatDelayMs = 450; @@ -310,9 +313,19 @@ void p4KeyboardPoll() { if (probe || drain_due) { if (TwoWire* w = p4I2c1Acquire(P4_I2C1_KEYBOARD, kBusWaitMs)) { if (probe) { - s_next_probe_ms = now + kHotplugMs; + // Back off while nothing is there. The probe is a real I2C transaction, + // and on an empty bus Arduino's Wire layer logs TWO [E] lines for every + // attempt -- at 1.5 s that is a permanent drip down the same USB-CDC the + // companion app talks over, for a board most people have never clipped + // on. After a few misses, look every kHotplugIdleMs instead; a hit puts + // it straight back to the responsive rate, so hot-plug still works, it + // just takes up to that long to notice. + s_next_probe_ms = now + (s_misses >= kMissesBeforeBackoff ? kHotplugIdleMs + : kHotplugMs); uint8_t cfg = 0; const bool there = regRead(w, P4KB_EXP_ADDR, XL_CFG0, cfg); + if (there) s_misses = 0; + else if (s_misses < kMissesBeforeBackoff) ++s_misses; // One-shot bus scan on the first probe. "It does not work" on this // expansion has three very different causes and they are impossible to // tell apart from the outside: the board is not clipped on, it is