On units whose C6 runs LilyGo's factory ESP-AT image, Wi-Fi died after about an hour: scans came back empty, Connect did nothing, and only a reboot helped. The AT worker brought the SDIO link up once at boot and never again, so a C6 that wedged mid-session left every command timing out for the rest of the session. Link health: AT commands now report OK / ERROR / TIMEOUT / BUSY. A timeout or busy reply asks for a plain "AT" ping; a ping that times out, or three command timeouts in a row with no answer between them, is a stall. ERROR replies are answers and never count. Connects to dead hosts (CIPSTART) and missing CIPSEND prompts only ask for a ping. The decision logic lives in c6_link_health.h with a host test. Recovery: on a stall the worker takes the link down at once (queued requests fail fast, links read closed so the TCP/WS companion servers prune them), then runs sdio_bring_down + the XL9535 C6 reset + c6at_begin, the same path as boot. Resets back off 30 s, 60 s, 120 s, 240 s, then every 5 min; 10 min of a healthy link resets the ladder. After a successful reset it rejoins the last network and the kept listener want-flag re-arms AT+CIPSERVER, so port 5000 comes back without app-side changes. The Wi-Fi status reads "reconnecting" meanwhile, and main.cpp no longer treats the reset as "radio off". Join/disconnect: one latest-wins slot instead of the 6-deep queue, so the user's Connect can no longer be dropped behind 10 s retry pairs and a stale CWQAP can no longer undo a fresh join. c6at_join_busy() gates main.cpp's retry while a join is pending or running. Bring-up now also sends AT+SLEEP=0 and sets AT+CWRECONNCFG=5,0 when the AT build supports it (query first; ERROR on old images is ignored). The esp-hosted build is unchanged: everything is in the c6_at component or behind !TDP4_C6_HOSTED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.5 KiB
C++
109 lines
4.5 KiB
C++
// Host test for the T-Display P4 legacy ESP-AT link-health state machine (#685).
|
|
// Build + run from the repo root:
|
|
// c++ -std=c++17 -Wall -Wextra -I tdisplay_p4/components/c6_at test/test_c6_link_health.cpp \
|
|
// -o /tmp/test_c6_link_health && /tmp/test_c6_link_health
|
|
#include <assert.h>
|
|
#include <stdio.h>
|
|
|
|
#include "c6_link_health.h"
|
|
|
|
static const uint32_t S = 1000; // ms per second
|
|
|
|
int main() {
|
|
// ---- backoff ladder: 30 s, 60 s, 120 s, 240 s, then capped at 5 min ----
|
|
assert(c6h_backoff_ms(0) == 30 * S);
|
|
assert(c6h_backoff_ms(1) == 60 * S);
|
|
assert(c6h_backoff_ms(2) == 120 * S);
|
|
assert(c6h_backoff_ms(3) == 240 * S);
|
|
assert(c6h_backoff_ms(4) == 300 * S);
|
|
assert(c6h_backoff_ms(200) == 300 * S);
|
|
|
|
c6h_t h;
|
|
c6h_init(&h);
|
|
|
|
// ---- ERROR is an answer: never counts toward a stall ----
|
|
for (int i = 0; i < 10; i++) assert(c6h_note(&h, C6H_RES_ERR, C6H_KIND_CMD, 0) == C6H_ACT_NONE);
|
|
assert(h.streak == 0);
|
|
|
|
// ---- timeouts: ping after each, stall on the third in a row ----
|
|
assert(c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_CMD, 0) == C6H_ACT_PING);
|
|
assert(c6h_note(&h, C6H_RES_OK, C6H_KIND_PING, 0) == C6H_ACT_NONE); // parser alive...
|
|
assert(h.streak == 1); // ...streak NOT cleared
|
|
assert(c6h_note(&h, C6H_RES_BUSY, C6H_KIND_CMD, 0) == C6H_ACT_PING); // busy counts too
|
|
assert(c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_CMD, 0) == C6H_ACT_STALL);
|
|
|
|
// ---- an answer in between resets the streak ----
|
|
c6h_init(&h);
|
|
c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_CMD, 0);
|
|
c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_CMD, 0);
|
|
assert(c6h_note(&h, C6H_RES_ERR, C6H_KIND_CMD, 0) == C6H_ACT_NONE);
|
|
assert(h.streak == 0);
|
|
assert(c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_CMD, 0) == C6H_ACT_PING);
|
|
|
|
// ---- a ping that times out is an immediate stall; busy/ERR pings are alive ----
|
|
c6h_init(&h);
|
|
assert(c6h_note(&h, C6H_RES_BUSY, C6H_KIND_PING, 0) == C6H_ACT_NONE);
|
|
assert(c6h_note(&h, C6H_RES_ERR, C6H_KIND_PING, 0) == C6H_ACT_NONE);
|
|
assert(c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_PING, 0) == C6H_ACT_STALL);
|
|
|
|
// ---- SOFT timeouts (connect to a dead host) only ask for a ping, never stall ----
|
|
c6h_init(&h);
|
|
for (int i = 0; i < 10; i++)
|
|
assert(c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_SOFT, 0) == C6H_ACT_PING);
|
|
assert(h.streak == 0);
|
|
// ...and a SOFT answer does not clear a CMD streak (it says nothing about the AT stack's state)
|
|
c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_CMD, 0);
|
|
c6h_note(&h, C6H_RES_OK, C6H_KIND_SOFT, 0);
|
|
assert(h.streak == 1);
|
|
|
|
// ---- reset scheduling: first reset immediate, then 30/60/120/240/300/300 s apart ----
|
|
c6h_init(&h);
|
|
uint32_t t = 5000 * S;
|
|
assert(c6h_reset_due(&h, t));
|
|
c6h_on_reset(&h, t); // reset #1 at t
|
|
assert(h.streak == 0);
|
|
assert(!c6h_reset_due(&h, t + 29 * S));
|
|
assert(c6h_reset_due(&h, t + 30 * S));
|
|
const uint32_t gaps[] = {30, 60, 120, 240, 300, 300, 300};
|
|
for (unsigned i = 0; i < sizeof(gaps) / sizeof(gaps[0]); i++) {
|
|
assert(!c6h_reset_due(&h, t + gaps[i] * S - 1));
|
|
t += gaps[i] * S;
|
|
assert(c6h_reset_due(&h, t));
|
|
c6h_on_reset(&h, t);
|
|
}
|
|
|
|
// ---- stability: 10 min of answers after a reset drops the ladder back to "immediate" ----
|
|
c6h_init(&h);
|
|
t = 100 * S;
|
|
c6h_on_reset(&h, t);
|
|
c6h_on_reset(&h, t + 30 * S); // two resets: next gap would be 60 s
|
|
t += 30 * S;
|
|
c6h_note(&h, C6H_RES_OK, C6H_KIND_CMD, t + 9 * 60 * S);
|
|
assert(h.resets == 2); // 9 min: not stable yet
|
|
assert(!c6h_reset_due(&h, t + 59 * S));
|
|
c6h_note(&h, C6H_RES_OK, C6H_KIND_CMD, t + 10 * 60 * S);
|
|
assert(h.resets == 0); // 10 min: ladder reset
|
|
assert(c6h_reset_due(&h, t + 10 * 60 * S + 1));
|
|
|
|
// ---- a re-stall soon after a successful reset still honours the backoff ----
|
|
c6h_init(&h);
|
|
t = 1000 * S;
|
|
c6h_on_reset(&h, t);
|
|
c6h_note(&h, C6H_RES_OK, C6H_KIND_CMD, t + 5 * S); // link came back
|
|
c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_CMD, t + 6 * S);
|
|
c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_CMD, t + 7 * S);
|
|
assert(c6h_note(&h, C6H_RES_TIMEOUT, C6H_KIND_CMD, t + 8 * S) == C6H_ACT_STALL);
|
|
assert(!c6h_reset_due(&h, t + 8 * S)); // wait for t + 30 s
|
|
assert(c6h_reset_due(&h, t + 30 * S));
|
|
|
|
// ---- millisecond clock wrap (esp_timer ms in a uint32_t wraps after ~49.7 days) ----
|
|
c6h_init(&h);
|
|
t = 0xFFFFFFFFu - 10 * S;
|
|
c6h_on_reset(&h, t);
|
|
assert(!c6h_reset_due(&h, t + 29 * S)); // wrapped past zero, still inside the window
|
|
assert(c6h_reset_due(&h, t + 30 * S));
|
|
|
|
printf("c6_link_health: all tests passed\n");
|
|
return 0;
|
|
}
|