/* * DeskLock — living-room face and voice for the Tatlock butler. * * Boot sequence (planned): * 1. BSP init: display + LVGL, touch, audio codec (ES8311/ES7210) * 2. Wi-Fi up via ESP32-C6 (ESP-Hosted over SDIO) * 3. WebSocket connection to the DeskLock gateway * 4. Face state machine: idle / listening / thinking / speaking * * Current stage: bring-up — display + a placeholder face. */ #include #include #include "esp_log.h" #include "bsp/esp-bsp.h" #include "esp_codec_dev.h" #include "lvgl.h" static const char *TAG = "desklock"; #define GONG_RATE 16000 #define GONG_SECONDS 5 #define GONG_SAMPLES (GONG_RATE * GONG_SECONDS) #define GONG_VOLUME 75 /* codec volume: discreet, not loud */ #define GONG_PEAK 14000.0f /* ~-7 dBFS headroom */ /* A distant temple gong in a stone space. * * Inharmonic partials with per-partial decay (lows ring for seconds, highs * die fast), a detuned fundamental pair for slow shimmer, a soft 45 ms * attack so the strike reads as far away, and three early reflections that * stand in for cathedral walls. Fundamental sits at 220 Hz — low enough to * feel voluminous, high enough for the bare 2 W speaker to reproduce. */ static const struct { float ratio; float amp; float tau; /* seconds to 1/e */ } gong_partials[] = { { 1.000f, 0.30f, 2.6f }, { 1.004f, 0.22f, 2.2f }, /* beating partner -> shimmer */ { 1.590f, 0.22f, 1.4f }, { 2.140f, 0.16f, 0.9f }, { 2.760f, 0.10f, 0.55f }, { 3.570f, 0.05f, 0.35f }, { 4.800f, 0.03f, 0.22f }, }; static const struct { int delay_ms; float gain; } gong_echoes[] = { { 95, 0.45f }, { 210, 0.30f }, { 370, 0.18f } }; static void play_boot_gong(void) { esp_codec_dev_handle_t spk = bsp_audio_codec_speaker_init(); if (spk == NULL) { ESP_LOGW(TAG, "speaker init failed — no gong"); return; } esp_codec_dev_sample_info_t fs = { .sample_rate = GONG_RATE, .channel = 1, .bits_per_sample = 16, }; esp_codec_dev_set_out_vol(spk, GONG_VOLUME); esp_codec_dev_open(spk, &fs); float *dry = heap_caps_calloc(GONG_SAMPLES, sizeof(float), MALLOC_CAP_SPIRAM); int16_t *pcm = heap_caps_malloc(GONG_SAMPLES * sizeof(int16_t), MALLOC_CAP_SPIRAM); if (dry == NULL || pcm == NULL) { ESP_LOGW(TAG, "no memory for gong"); goto out; } for (size_t p = 0; p < sizeof(gong_partials) / sizeof(gong_partials[0]); p++) { float step = 2.0f * (float)M_PI * 220.0f * gong_partials[p].ratio / GONG_RATE; float decay = expf(-1.0f / (gong_partials[p].tau * GONG_RATE)); float env = gong_partials[p].amp; float phase = 0.0f; for (int i = 0; i < GONG_SAMPLES; i++) { phase += step; env *= decay; dry[i] += sinf(phase) * env; } } const int attack = GONG_RATE * 45 / 1000; /* distant, not percussive */ for (int i = 0; i < attack; i++) { dry[i] *= (float)i / attack; } float peak = 1e-6f; for (int i = 0; i < GONG_SAMPLES; i++) { float s = dry[i]; for (size_t e = 0; e < sizeof(gong_echoes) / sizeof(gong_echoes[0]); e++) { int j = i - GONG_RATE * gong_echoes[e].delay_ms / 1000; if (j >= 0) { s += dry[j] * gong_echoes[e].gain; } } const int fade = GONG_RATE * 2 / 5; /* last 400 ms to silence */ if (i > GONG_SAMPLES - fade) { s *= (float)(GONG_SAMPLES - i) / fade; } dry[i] = s; if (fabsf(s) > peak) { peak = fabsf(s); } } for (int i = 0; i < GONG_SAMPLES; i++) { pcm[i] = (int16_t)(dry[i] * (GONG_PEAK / peak)); } esp_codec_dev_write(spk, pcm, GONG_SAMPLES * sizeof(int16_t)); ESP_LOGI(TAG, "boot gong played"); out: free(dry); free(pcm); esp_codec_dev_close(spk); } static void show_placeholder_face(void) { bsp_display_lock(0); lv_obj_t *scr = lv_screen_active(); lv_obj_set_style_bg_color(scr, lv_color_black(), 0); lv_obj_t *eyes = lv_label_create(scr); lv_label_set_text(eyes, "- -"); lv_obj_set_style_text_color(eyes, lv_color_hex(0xADFFC8), 0); lv_obj_set_style_text_font(eyes, &lv_font_montserrat_48, 0); lv_obj_align(eyes, LV_ALIGN_CENTER, 0, -80); lv_obj_t *mouth = lv_label_create(scr); lv_label_set_text(mouth, "\\_/"); lv_obj_set_style_text_color(mouth, lv_color_hex(0xADFFC8), 0); lv_obj_set_style_text_font(mouth, &lv_font_montserrat_48, 0); lv_obj_align(mouth, LV_ALIGN_CENTER, 0, 40); lv_obj_t *tag = lv_label_create(scr); lv_label_set_text(tag, "DESKLOCK"); lv_obj_set_style_text_color(tag, lv_color_hex(0x2F7A4B), 0); lv_obj_align(tag, LV_ALIGN_CENTER, 0, 160); bsp_display_unlock(); } void app_main(void) { ESP_LOGI(TAG, "DeskLock starting"); bsp_display_start(); bsp_display_backlight_on(); show_placeholder_face(); play_boot_gong(); ESP_LOGI(TAG, "DeskLock up (placeholder face)"); /* TODO(face): full LVGL face state machine per docs/architecture.md * TODO(net): Wi-Fi via ESP-Hosted (C6), WebSocket to gateway * TODO(voice): ES7210 capture upstream, ES8311 playback downstream */ }