/* DeskLock — living-room face and voice for the Tatlock butler. * * 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/queue.h" #include "freertos/semphr.h" #include "freertos/task.h" #include "bsp/esp-bsp.h" #include "desklock.h" /* 1 = L1 wifi driver diagnosis (SoftAP, no STA/gateway). 0 = normal app. */ #define WIFI_DIAG_MODE 0 static const char *TAG = "desklock"; /* All utterance framing (blocking WS sends, chime, face) runs on app_task, never * on the LVGL touch callback nor the AFE detect/fetch thread. The wake word, VAD, * and touch just POST an event; app_task is the single serializer of start/end, * which removes both the fetch-thread stall and the double-start race. */ typedef enum { EV_TOUCH, EV_WAKE, EV_SPEECH_END } app_event_t; static QueueHandle_t s_events; static void post_event(app_event_t ev) { if (s_events != NULL) { xQueueSend(s_events, &ev, 0); } } /* --- these three are the fast, non-blocking hooks called from other tasks --- */ void app_on_touch(void) { post_event(EV_TOUCH); } /* LVGL touch callback */ void app_on_wake(void) { post_event(EV_WAKE); } /* AFE detect_task: wake word */ void app_on_speech_end(void) { post_event(EV_SPEECH_END); } /* AFE detect_task: VAD silence */ /* --- the actual work, all on app_task --- */ static void start_utterance(void) { if (!gw_connected() || audio_is_playing() || audio_capture_active() || face_get() == FACE_EFFORT) { return; } face_activity(); audio_play_chime(); /* audible "I heard you" */ gw_send_text("{\"type\":\"utterance_start\"}"); /* must precede any audio frames */ audio_capture_start(); /* now detect_task forwards audio */ face_set(FACE_LISTENING); ESP_LOGI(TAG, "listening…"); } static void end_utterance(void) { if (!audio_capture_active()) { return; } audio_capture_stop(); gw_send_text("{\"type\":\"utterance_end\"}"); face_set(FACE_PENSIVE); ESP_LOGI(TAG, "utterance sent"); } /* WS link dropped: abandon any half-utterance so the next wake starts fresh. */ void app_on_disconnect(void) { audio_capture_stop(); } static void app_task(void *arg) { (void)arg; app_event_t ev; for (;;) { if (xQueueReceive(s_events, &ev, portMAX_DELAY) != pdTRUE) { continue; } if (ev == EV_TOUCH) { vTaskDelay(pdMS_TO_TICKS(60)); /* debounce */ xQueueReset(s_events); /* coalesce a bouncy tap burst */ face_activity(); if (!gw_connected()) { audio_capture_stop(); } else if (!audio_capture_active()) { start_utterance(); /* tap = manual wake */ } else { end_utterance(); /* tap = manual end */ } } else if (ev == EV_WAKE) { start_utterance(); } else if (ev == EV_SPEECH_END) { end_utterance(); } } } void app_on_playback_done(void) { face_set(FACE_IDLE); } #if SDIO_TX_SELFTEST /* Temporary autonomous SDIO-TX stress test: stream mic audio upstream for 40s * right after connecting (no user tap needed) to prove the #167 alignment fix. * Remove once verified. */ static void sdio_tx_selftest_task(void *arg) { (void)arg; vTaskDelay(pdMS_TO_TICKS(4000)); ESP_LOGW("selftest", "SDIO TX STRESS START: streaming mic audio 40s"); audio_capture_start(); for (int i = 0; i < 40; i++) { vTaskDelay(pdMS_TO_TICKS(1000)); ESP_LOGW("selftest", "SDIO TX STRESS: alive %ds (no wedge)", i + 1); } audio_capture_stop(); ESP_LOGW("selftest", "SDIO TX STRESS SURVIVED 40s — #167 FIX CONFIRMED"); vTaskDelete(NULL); } void sdio_tx_selftest_kick(void) { static bool started; if (!started) { started = true; xTaskCreate(sdio_tx_selftest_task, "sdiotest", 4096, NULL, 4, NULL); } } #endif void app_main(void) { ESP_LOGI(TAG, "DeskLock starting"); s_events = xQueueCreate(8, sizeof(app_event_t)); bsp_display_start(); bsp_display_backlight_on(); face_init(); audio_init(); if (esp_reset_reason() == ESP_RST_POWERON) { 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 net_start(); c6_ota_start(); /* one-shot C6 radio firmware update — remove once proven */ #endif ESP_LOGI(TAG, "DeskLock up"); }