Fix touch crash + make rain a reachability signal + calm the alarm churn
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:
+15
-7
@@ -35,6 +35,7 @@ static volatile bool s_capturing;
|
||||
|
||||
typedef enum { JOB_GONG, JOB_REPLY } job_t;
|
||||
static QueueHandle_t s_jobs;
|
||||
static TaskHandle_t s_capture_task;
|
||||
|
||||
/* --- gong: see docs/architecture.md "Sound signature" --- */
|
||||
|
||||
@@ -126,17 +127,23 @@ static void audio_task(void *arg)
|
||||
}
|
||||
}
|
||||
|
||||
/* One persistent capture task, created once. It parks on a notification until
|
||||
* capture is requested, then streams mic -> gateway while s_capturing holds.
|
||||
* This makes capture single-instance: no create/delete restart race, so a rapid
|
||||
* re-tap (or a WS-disconnect stop racing an app_task start) can never put two
|
||||
* readers on the one mic/I2S handle. */
|
||||
static void capture_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
uint8_t *chunk = heap_caps_malloc(CAPTURE_CHUNK, MALLOC_CAP_DEFAULT);
|
||||
while (s_capturing) {
|
||||
if (esp_codec_dev_read(s_mic, chunk, CAPTURE_CHUNK) == ESP_CODEC_DEV_OK) {
|
||||
gw_send_bin(chunk, CAPTURE_CHUNK);
|
||||
for (;;) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
while (s_capturing) {
|
||||
if (esp_codec_dev_read(s_mic, chunk, CAPTURE_CHUNK) == ESP_CODEC_DEV_OK) {
|
||||
gw_send_bin(chunk, CAPTURE_CHUNK);
|
||||
}
|
||||
}
|
||||
}
|
||||
free(chunk);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void audio_init(void)
|
||||
@@ -159,6 +166,7 @@ void audio_init(void)
|
||||
s_reply = heap_caps_malloc(REPLY_MAX, MALLOC_CAP_SPIRAM);
|
||||
s_jobs = xQueueCreate(4, sizeof(job_t));
|
||||
xTaskCreate(audio_task, "audio", 4096, NULL, 5, NULL);
|
||||
xTaskCreate(capture_task, "capture", 4096, NULL, 6, &s_capture_task);
|
||||
ESP_LOGI(TAG, "audio up (spk=%d mic=%d)", s_spk != NULL, s_mic != NULL);
|
||||
}
|
||||
|
||||
@@ -170,11 +178,11 @@ void audio_play_gong(void)
|
||||
|
||||
void audio_capture_start(void)
|
||||
{
|
||||
if (s_mic == NULL || s_capturing) {
|
||||
if (s_mic == NULL || s_capturing || s_capture_task == NULL) {
|
||||
return;
|
||||
}
|
||||
s_capturing = true;
|
||||
xTaskCreate(capture_task, "capture", 4096, NULL, 6, NULL);
|
||||
xTaskNotifyGive(s_capture_task); /* wake the persistent task's inner loop */
|
||||
}
|
||||
|
||||
void audio_capture_stop(void)
|
||||
|
||||
@@ -50,4 +50,5 @@ void c6_ota_start(void);
|
||||
|
||||
/* desklock_main.c */
|
||||
void app_on_touch(void);
|
||||
void app_on_disconnect(void);
|
||||
void app_on_playback_done(void);
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
*
|
||||
* Boot: display + face -> audio (gong) -> Wi-Fi (C6/ESP-Hosted) -> gateway WS.
|
||||
* Interaction (phase 2): touch-to-talk — tap to speak, tap again to finish.
|
||||
*
|
||||
* Touch is DECOUPLED from the LVGL thread: the touch callback only signals a
|
||||
* semaphore; all the real work (blocking WebSocket sends, audio, face changes)
|
||||
* runs in app_task. A blocking WS send on the LVGL render thread stalls the
|
||||
* MIPI-DSI flush -> blue/garbage frame + task-watchdog reboot.
|
||||
*/
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
#include "bsp/esp-bsp.h"
|
||||
|
||||
#include "desklock.h"
|
||||
@@ -15,13 +23,31 @@
|
||||
|
||||
static const char *TAG = "desklock";
|
||||
|
||||
static SemaphoreHandle_t s_touch_sem;
|
||||
static bool s_talking;
|
||||
|
||||
/* Called from the LVGL touch callback — MUST be fast and non-blocking. */
|
||||
void app_on_touch(void)
|
||||
{
|
||||
if (s_touch_sem != NULL) {
|
||||
xSemaphoreGive(s_touch_sem);
|
||||
}
|
||||
}
|
||||
|
||||
/* Called from the WS event task when the link drops: forget any half-utterance
|
||||
* so the next tap starts a fresh one instead of hitting the "stop" branch. */
|
||||
void app_on_disconnect(void)
|
||||
{
|
||||
s_talking = false;
|
||||
}
|
||||
|
||||
/* Runs in app_task (normal context): blocking sends here don't stall rendering. */
|
||||
static void handle_touch(void)
|
||||
{
|
||||
face_activity();
|
||||
|
||||
if (!gw_connected()) {
|
||||
s_talking = false; /* link gone; nothing to talk to */
|
||||
return;
|
||||
}
|
||||
if (!s_talking) {
|
||||
@@ -42,17 +68,29 @@ void app_on_touch(void)
|
||||
}
|
||||
}
|
||||
|
||||
static void app_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
if (xSemaphoreTake(s_touch_sem, portMAX_DELAY) == pdTRUE) {
|
||||
vTaskDelay(pdMS_TO_TICKS(60)); /* debounce */
|
||||
xSemaphoreTake(s_touch_sem, 0); /* coalesce repeats during debounce */
|
||||
handle_touch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void app_on_playback_done(void)
|
||||
{
|
||||
#if !WIFI_DIAG_MODE
|
||||
face_set(FACE_IDLE); /* in diag mode the status line must stay visible */
|
||||
#endif
|
||||
face_set(FACE_IDLE);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "DeskLock starting");
|
||||
|
||||
s_touch_sem = xSemaphoreCreateBinary();
|
||||
|
||||
bsp_display_start();
|
||||
bsp_display_backlight_on();
|
||||
face_init();
|
||||
@@ -62,6 +100,8 @@ void app_main(void)
|
||||
audio_play_gong(); /* cold boot only — recovery reboots stay silent */
|
||||
}
|
||||
|
||||
xTaskCreate(app_task, "app", 4096, NULL, 5, NULL);
|
||||
|
||||
#if WIFI_DIAG_MODE
|
||||
wifi_diag_start();
|
||||
#else
|
||||
|
||||
+27
-4
@@ -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;
|
||||
|
||||
@@ -80,7 +80,10 @@ static void ws_event(void *arg, esp_event_base_t base, int32_t id, void *data)
|
||||
}
|
||||
s_connected = false;
|
||||
audio_capture_stop();
|
||||
face_set(FACE_ERROR);
|
||||
app_on_disconnect();
|
||||
/* Stay calm: the rain draining away signals "unreachable". A sustained
|
||||
* outage escalates to the x_x error face in face.c (clock_cb). */
|
||||
face_set(FACE_IDLE);
|
||||
break;
|
||||
case WEBSOCKET_EVENT_DATA:
|
||||
if (ev->op_code == 0x01) { /* text */
|
||||
@@ -120,13 +123,13 @@ bool gw_connected(void)
|
||||
void gw_send_text(const char *json)
|
||||
{
|
||||
if (s_connected) {
|
||||
esp_websocket_client_send_text(s_ws, json, strlen(json), pdMS_TO_TICKS(5000));
|
||||
esp_websocket_client_send_text(s_ws, json, strlen(json), pdMS_TO_TICKS(1500));
|
||||
}
|
||||
}
|
||||
|
||||
void gw_send_bin(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (s_connected) {
|
||||
esp_websocket_client_send_bin(s_ws, (const char *)data, len, pdMS_TO_TICKS(5000));
|
||||
esp_websocket_client_send_bin(s_ws, (const char *)data, len, pdMS_TO_TICKS(1500));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ static void wifi_event(void *arg, esp_event_base_t base, int32_t id, void *data)
|
||||
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
wifi_event_sta_disconnected_t *dc = (wifi_event_sta_disconnected_t *)data;
|
||||
ESP_LOGW(TAG, "wifi disconnected (reason %d), retrying", dc ? dc->reason : -1);
|
||||
face_set(FACE_ERROR);
|
||||
/* rain drains via the gw_connected() gate; face stays calm (see face.c) */
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
esp_wifi_connect();
|
||||
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
|
||||
|
||||
Reference in New Issue
Block a user