diff --git a/firmware/main/desklock_main.c b/firmware/main/desklock_main.c index fbde868..9b80383 100644 --- a/firmware/main/desklock_main.c +++ b/firmware/main/desklock_main.c @@ -22,6 +22,13 @@ /* 1 = L1 wifi driver diagnosis (SoftAP, no STA/gateway). 0 = normal app. */ #define WIFI_DIAG_MODE 0 +/* 1 = deterministic load-emulation harness for the blue-flicker RCA: cycles + * isolated load types (render-only ladder, light-render+network, heavy-render+ + * network, heavy-render+playback) with a log marker per phase, so we can see + * which load starves the DSI. Kept (gated off) for future bus-contention debugging. + * 0 = normal app. */ +#define FACE_LOADTEST 0 + static const char *TAG = "desklock"; /* All utterance framing (blocking WS sends, chime, face) runs on app_task, never @@ -107,6 +114,94 @@ void app_on_playback_done(void) face_set(FACE_IDLE); } +#if FACE_LOADTEST +/* Stream real mic audio upstream for `ms` — exercises the network path (I2S-in + * DMA + esp-hosted SDIO TX, both touching PSRAM) with valid utterance framing so + * the gateway keeps the WS open. */ +static void loadtest_stream(int ms) +{ + if (!gw_connected()) { + vTaskDelay(pdMS_TO_TICKS(ms)); + return; + } + gw_send_text("{\"type\":\"utterance_start\"}"); + audio_capture_start(); + vTaskDelay(pdMS_TO_TICKS(ms)); + audio_capture_stop(); + gw_send_text("{\"type\":\"utterance_end\"}"); +} + +static void loadtest_task(void *arg) +{ + (void)arg; + vTaskDelay(pdMS_TO_TICKS(12000)); /* let boot + Wi-Fi + gateway settle */ + const struct { + face_state_t st; + const char *name; + } LADDER[] = { + { FACE_IDLE, "IDLE(4)" }, { FACE_PENSIVE, "PENSIVE(7)" }, + { FACE_SPEAKING, "SPEAKING(14)" }, { FACE_LISTENING, "LISTENING(16)" }, + { FACE_RAGE, "RAGE(34)" }, { FACE_EFFORT, "EFFORT(40)" }, + }; + char tag[24]; + static uint8_t netframe[1024] = {0}; + for (;;) { + ESP_LOGW("loadtest", "=== P1 render-only ladder (NO network) ==="); + for (int i = 0; i < 6; i++) { + ESP_LOGW("loadtest", "P1: %s", LADDER[i].name); + face_set(LADDER[i].st); + snprintf(tag, sizeof(tag), "P1 %s", LADDER[i].name); + face_status(tag); /* phase label on screen */ + vTaskDelay(pdMS_TO_TICKS(6000)); + } + ESP_LOGW("loadtest", "=== P2 LIGHT render (IDLE 4) + NETWORK stream — key test ==="); + face_set(FACE_IDLE); + face_status("P2 net + idle"); + loadtest_stream(8000); + vTaskDelay(pdMS_TO_TICKS(6000)); /* let any reply drain */ + + ESP_LOGW("loadtest", "=== P3 HEAVY render (EFFORT 40) + NETWORK stream ==="); + face_set(FACE_EFFORT); + face_status("P3 net + EFFORT"); + loadtest_stream(8000); + vTaskDelay(pdMS_TO_TICKS(6000)); + + ESP_LOGW("loadtest", "=== P4 HEAVY render (EFFORT 40) + PLAYBACK dma ==="); + face_set(FACE_EFFORT); + face_status("P4 play + EFFORT"); + for (int i = 0; i < 3; i++) { + audio_play_gong(); + vTaskDelay(pdMS_TO_TICKS(2500)); + } + + /* P5: the real-conversation worst case — heavy render + audio playback + + * network stream ALL AT ONCE, like the SPEAKING state (TTS audio arriving + * over WS while it plays and the rain runs). Render (P1) and each DMA source + * alone (P3/P4) were clean at 48 MHz; this stacks them to reproduce the live + * flicker. utterance framing keeps the WS accepting the frames. */ + ESP_LOGW("loadtest", "=== P5 EFFORT + PLAYBACK + NETWORK (full-reply emulation) ==="); + face_set(FACE_EFFORT); + face_status("P5 play+net+render"); + gw_send_text("{\"type\":\"utterance_start\"}"); + for (int t = 0; t < 550; t++) { /* ~8s combined load */ + if (!audio_is_playing()) { + audio_play_gong(); /* I2S-out playback DMA */ + face_set(FACE_EFFORT); /* playback-done flips to IDLE */ + } + if (gw_connected()) { + gw_send_bin(netframe, sizeof(netframe)); /* SDIO/mempool DMA */ + } + vTaskDelay(pdMS_TO_TICKS(15)); + } + gw_send_text("{\"type\":\"utterance_end\"}"); + vTaskDelay(pdMS_TO_TICKS(4000)); + + ESP_LOGW("loadtest", "=== cycle complete, repeating in 3s ==="); + vTaskDelay(pdMS_TO_TICKS(3000)); + } +} +#endif + #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. @@ -142,17 +237,23 @@ void app_main(void) s_events = xQueueCreate(8, sizeof(app_event_t)); - /* TRIPLE_FULL, not the BSP default TRIPLE_PARTIAL: on IDF 5.5 the DSI driver - * lacks the on_frame_buf_complete callback, so PARTIAL mode falls back to a - * per-refresh "fake vsync" that over-releases the scanout buffer when a frame - * takes >1 refresh to draw (heavy rain + always-on WakeNet) -> tearing/flicker. - * TRIPLE_FULL is submit-gated even without that callback. Same 3 framebuffers. */ + /* Same config bsp_display_start() uses (TRIPLE_PARTIAL, no rotation) but with a + * 16 KB LVGL task stack instead of the 8 KB default. + * + * The blue flicker's real cause is a DSI framebuffer-read underrun on the PSRAM + * bus (fixed primarily by the reduced DPI clock in the BSP). This 16 KB stack + * pairs with LV_INV_BUF_SIZE=128 (top CMakeLists), a SECONDARY bandwidth + * mitigation: 128 keeps the busy 40-stream rain as small partial redraws instead + * of collapsing into full-screen redraws, which would pile large PSRAM write + * bursts onto the DSI's read and re-provoke the underrun. The 128-entry inv + * buffer enlarges the flush stack frame, and 8 KB overflowed it by ~1 KB. */ bsp_display_cfg_t disp_cfg = { .lv_adapter_cfg = ESP_LV_ADAPTER_DEFAULT_CONFIG(), .rotation = ESP_LV_ADAPTER_ROTATE_0, - .tear_avoid_mode = ESP_LV_ADAPTER_TEAR_AVOID_MODE_TRIPLE_FULL, + .tear_avoid_mode = ESP_LV_ADAPTER_TEAR_AVOID_MODE_TRIPLE_PARTIAL, .touch_flags = { .swap_xy = 0, .mirror_x = 0, .mirror_y = 0 }, }; + disp_cfg.lv_adapter_cfg.task_stack_size = 16 * 1024; bsp_display_start_with_config(&disp_cfg); bsp_display_backlight_on(); face_init(); @@ -172,4 +273,8 @@ void app_main(void) #endif ESP_LOGI(TAG, "DeskLock up"); + +#if FACE_LOADTEST + xTaskCreate(loadtest_task, "loadtest", 4096, NULL, 4, NULL); +#endif }