Fix touch crash + make rain a reachability signal + calm the alarm churn
Test, Build and Push / test-gateway (push) Successful in 10s
Test, Build and Push / release (push) Skipped
Test, Build and Push / build-gateway (push) Skipped

TOUCH CRASH (the blue screen): app_on_touch ran a blocking WebSocket
send (up to 5s) directly in the LVGL touch callback, stalling the
MIPI-DSI flush into a garbage/blue frame + task-watchdog reboot on
every tap. Now touch_cb only gives a semaphore; a dedicated app_task
does the blocking sends, audio, and face changes off the render
thread. WS send timeout cut 5s to 1.5s as belt-and-braces.

RAIN = REACHABILITY (user request): rain now falls only while the
gateway WebSocket is live (gated on gw_connected in rain_tick). It
drains gracefully on disconnect, resumes on reconnect, a genuine
glanceable reachable signal. Idle density bumped 2 to 4 so
connected-idle reads distinctly from disconnected-black.

CALM THE WEDGE CHURN (user request): transient wifi/WS drops no longer
slam to the x_x error face or a CONNECTING banner. Boot goes straight
to the calm idle face (dry until connected). Only a sustained 30s+
outage escalates to x_x (clock_cb); the ~15s wedge-recovery just shows
a brief rain pause.

Two fixes from adversarial concurrency review before flashing:
- persistent single capture task (was xTaskCreate per utterance; a
  rapid re-tap or WS-stop-vs-app-start race could put two readers on
  one mic/I2S handle and corrupt the codec)
- reset s_talking on disconnect (app_on_disconnect) so the first tap
  after a mid-utterance drop starts fresh, not the stop branch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 09:04:57 +02:00
co-authored by Claude Fable 5
parent 795a1d4baf
commit 5c6a6eba36
6 changed files with 93 additions and 18 deletions
+27 -4
View File
@@ -39,7 +39,7 @@ typedef struct {
static const face_def_t DEFS[] = {
[FACE_BOOT] = { "- -", "\\_/", false, false, false, 6, 3 },
[FACE_IDLE] = { "- -", "\\_/", true, false, false, 2, 2 },
[FACE_IDLE] = { "- -", "\\_/", true, false, false, 4, 2 },
[FACE_LISTENING] = { "O O", "o", true, false, false, 16, 4 },
[FACE_PENSIVE] = { "\xC2\xB7 \xC2\xB7", "~", false, true, false, 7, 3 },
[FACE_EFFORT] = { "> <", "~", false, false, false, 40, 9 },
@@ -153,7 +153,9 @@ static void stream_hide(stream_t *s)
static void rain_tick(void)
{
const face_def_t *d = &DEFS[F.state];
int target = (F.power == 2) ? 0 : d->rain_streams;
/* Rain is the reachability signal: it only falls when the gateway is live.
* A disconnect drains it gracefully (streams finish falling, none respawn). */
int target = (F.power == 2 || !gw_connected()) ? 0 : d->rain_streams;
int alive = 0;
for (int i = 0; i < RAIN_MAX; i++) {
stream_t *s = &F.streams[i];
@@ -308,6 +310,24 @@ static void tick_cb(lv_timer_t *timer)
static void clock_cb(lv_timer_t *timer)
{
(void)timer;
/* Sustained-outage escalation: a brief wedge-recovery (~15 s) just drains
* the rain and keeps the calm face; only a real >30 s outage shows x_x. */
static uint32_t disc_since;
if (!gw_connected()) {
if (disc_since == 0) {
disc_since = now_ms();
}
if (now_ms() - disc_since > 30000 && F.state == FACE_IDLE) {
face_set(FACE_ERROR);
}
} else {
disc_since = 0;
if (F.state == FACE_ERROR) {
face_set(FACE_IDLE);
}
}
if (F.state != FACE_IDLE) {
lv_obj_add_flag(F.clock_lbl, LV_OBJ_FLAG_HIDDEN);
return;
@@ -373,8 +393,9 @@ void face_init(void)
F.status = lv_label_create(scr);
lv_obj_set_style_text_font(F.status, &lv_font_montserrat_28, 0);
lv_obj_set_style_text_color(F.status, lv_color_hex(0x2F7A4B), 0);
lv_label_set_text(F.status, "CONNECTING");
lv_label_set_text(F.status, "");
lv_obj_align(F.status, LV_ALIGN_CENTER, 0, 270);
lv_obj_add_flag(F.status, LV_OBJ_FLAG_HIDDEN); /* only shown in diag mode */
F.elapsed = lv_label_create(scr);
lv_obj_set_style_text_font(F.elapsed, &lv_font_montserrat_28, 0);
@@ -405,7 +426,9 @@ void face_init(void)
lv_obj_add_flag(touch, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(touch, touch_cb, LV_EVENT_CLICKED, NULL);
F.state = FACE_BOOT;
/* Boot straight into the calm idle face (dry — rain gate holds it off until
* the gateway connects). No "CONNECTING" banner churn on every recovery. */
F.state = FACE_IDLE;
F.last_activity = now_ms();
F.state_since = now_ms();
F.blink_in_ms = 3000;