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>
This commit is contained in:
Kaj SchittecatandClaude Opus 5 committed 2026-10-05 10:26:17 +02:00
1 parent 7ce1b3f48c
commit b52c71f4a5
3 files changed
+75 -6

No files matched your search

+47
View File
@@ -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
+14 -5
View File
@@ -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
+14 -1
View File
@@ -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