diff --git a/AGENTS.md b/AGENTS.md index 60a44a1..7a8922a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,25 @@ sg dialout -c "bash -lc 'source ~/esp-idf/export.sh >/dev/null && idf.py -p /dev - `sg dialout -c '…'` is needed because the login session predates the user's dialout membership; a plain `idf.py flash` works after any re-login. +- **Radio stack: esp_hosted ≥ 2.x on BOTH chips, non-negotiable.** esp-hosted 1.x is + formally incompatible with IDF 5.5 (esp-hosted-mcu#47) — symptom: RPC/scan/connect + all work, but NO data frames ever flow (no DHCP, no ARP, no ping). Waveshare's + examples pin 1.4.* and the factory C6 slave firmware is ancient — both wrong. The + host manifest pins `espressif/esp_hosted: "^2.12"`; the matching slave image is + embedded as `main/c6_slave.bin` and `c6_ota.c` flashes the C6 **over SDIO** at boot + whenever the C6 reports a version < 2.x (build a new bin from the component's + `slave/` project for esp32c6 when bumping versions). +- **Internal-RAM famine assert**: `assert failed: xTaskCreateStaticPinnedToCore … + xPortcheckValidStackMem` in a pre-app_main boot loop means static+early allocations + starved internal SRAM (hosted 2.x is hungry). Keep + `CONFIG_ESP_HOSTED_MEMPOOL_PREFER_SPIRAM=y` and the reduced `WIFI_RMT_*` buffer + counts in sdkconfig.defaults; check `heap_init:` pool lines when the binary grows. +- SDIO clock is set conservatively (`CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ=20000`), + ample for 16 kHz voice; raising to 40 MHz is untested on this board's data path. +- **L1 driver diagnosis mode**: set `WIFI_DIAG_MODE 1` in desklock_main.c — the device + becomes AP `DESKLOCK-DIAG` (pass `desklock123`, page at http://192.168.4.1/) proving + radio+SDIO+IP with zero external network variables. Ladder: L0 SDIO control → L1 + softap data → L2 STA to any network → L3 STA to "Outside" → L4 gateway. - **PSRAM must run at 200 MHz** or the 800×800 MIPI-DSI framebuffer underruns (`lcd.dsi.dpi: can't fetch data…` spam, LVGL lock never frees, task watchdog). `CONFIG_SPIRAM_SPEED_200M` only takes effect together with diff --git a/CLAUDE.md b/CLAUDE.md index 5717899..b53e4f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,10 @@ make lint typecheck - **PSRAM 200 MHz requires `CONFIG_IDF_EXPERIMENTAL_FEATURES=y`** — without it the option silently degrades to 20 MHz and the DSI display underruns into a watchdog loop. Details in AGENTS.md. +- **Wi-Fi = esp_hosted 2.x on BOTH chips** (host manifest + C6 slave, auto-OTA'd from + `main/c6_slave.bin`). 1.x on IDF 5.5 gives working control RPC but a dead data path + (the great July 14th debugging night). Boot-loop assert on + `xTaskCreateStaticPinnedToCore` = internal-RAM famine. Details in AGENTS.md. - Gateway speech deps are optional extras; `make setup` alone runs the app and tests without GPU/ML packages. `make setup` uses `python3.12` (system python3 is 3.8). - Tatlock replies open with a `` block — always strip via diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt index f587834..35bebc1 100644 --- a/firmware/main/CMakeLists.txt +++ b/firmware/main/CMakeLists.txt @@ -5,11 +5,14 @@ idf_component_register( "audio.c" "net.c" "gw_client.c" + "c6_ota.c" + "wifi_diag.c" "fonts/font_face_140.c" "fonts/font_rain_22.c" "fonts/font_rage_64.c" INCLUDE_DIRS "." - PRIV_REQUIRES nvs_flash esp_wifi esp_event esp_netif json esp_timer esp_app_format + EMBED_FILES "c6_slave.bin" + PRIV_REQUIRES nvs_flash esp_wifi esp_event esp_netif json esp_timer esp_app_format esp_http_server ) target_compile_definitions(${COMPONENT_LIB} PRIVATE LV_LVGL_H_INCLUDE_SIMPLE) diff --git a/firmware/main/c6_ota.c b/firmware/main/c6_ota.c new file mode 100644 index 0000000..983a09d --- /dev/null +++ b/firmware/main/c6_ota.c @@ -0,0 +1,87 @@ +/* One-shot ESP32-C6 slave firmware update over the SDIO RPC channel. + * + * The factory C6 firmware's data path never carried a frame (control RPC + * works; all data traffic dies). This streams a slave image built from the + * exact same esp_hosted version as the host driver, using the RPC OTA + * primitives that ride the (working) control path — no network involved. + * + * Remove the c6_ota_start() call from app_main once the radio is proven. + */ + +#include "esp_log.h" +#include "esp_system.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_hosted.h" + +#include "desklock.h" + +static const char *TAG = "c6ota"; + +extern int rpc_ota_begin(void); +extern int rpc_ota_write(uint8_t *ota_data, uint32_t ota_data_len); +extern int rpc_ota_end(void); + +extern const uint8_t slave_bin_start[] asm("_binary_c6_slave_bin_start"); +extern const uint8_t slave_bin_end[] asm("_binary_c6_slave_bin_end"); + +#define CHUNK 1400 + +static void log_c6_version(const char *when) +{ + esp_hosted_coprocessor_fwver_t v = { 0 }; + if (esp_hosted_get_coprocessor_fwversion(&v) == ESP_OK) { + ESP_LOGI(TAG, "C6 fw %s: %u.%u.%u", when, + (unsigned)v.major1, (unsigned)v.minor1, (unsigned)v.patch1); + } else { + ESP_LOGW(TAG, "C6 fw %s: version query failed", when); + } +} + +static void c6_ota_task(void *arg) +{ + (void)arg; + vTaskDelay(pdMS_TO_TICKS(8000)); /* let the hosted transport settle */ + esp_hosted_coprocessor_fwver_t v = { 0 }; + if (esp_hosted_get_coprocessor_fwversion(&v) == ESP_OK && + v.major1 >= 2) { + ESP_LOGI(TAG, "C6 fw already 2.x — no OTA needed"); + vTaskDelete(NULL); + return; + } + log_c6_version("before"); + + size_t total = slave_bin_end - slave_bin_start; + ESP_LOGI(TAG, "C6 OTA start: %u bytes", (unsigned)total); + if (rpc_ota_begin() != 0) { + ESP_LOGE(TAG, "ota_begin FAILED (factory partition table may lack OTA slots)"); + vTaskDelete(NULL); + return; + } + size_t off = 0; + while (off < total) { + uint32_t n = (total - off) < CHUNK ? (uint32_t)(total - off) : CHUNK; + if (rpc_ota_write((uint8_t *)(slave_bin_start + off), n) != 0) { + ESP_LOGE(TAG, "ota_write FAILED at %u/%u", (unsigned)off, (unsigned)total); + vTaskDelete(NULL); + return; + } + off += n; + if ((off / CHUNK) % 100 == 0) { + ESP_LOGI(TAG, "C6 OTA %u/%u", (unsigned)off, (unsigned)total); + } + } + if (rpc_ota_end() == 0) { + ESP_LOGI(TAG, "C6 OTA SUCCESS — restarting both chips"); + vTaskDelay(pdMS_TO_TICKS(3000)); + esp_restart(); + } else { + ESP_LOGE(TAG, "ota_end FAILED"); + } + vTaskDelete(NULL); +} + +void c6_ota_start(void) +{ + xTaskCreate(c6_ota_task, "c6ota", 6144, NULL, 4, NULL); +} diff --git a/firmware/main/c6_slave.bin b/firmware/main/c6_slave.bin new file mode 100644 index 0000000..14568f3 Binary files /dev/null and b/firmware/main/c6_slave.bin differ diff --git a/firmware/main/desklock.h b/firmware/main/desklock.h index ea936d4..076ca1a 100644 --- a/firmware/main/desklock.h +++ b/firmware/main/desklock.h @@ -20,7 +20,8 @@ typedef enum { void face_init(void); void face_set(face_state_t state); /* safe from any task */ face_state_t face_get(void); -void face_activity(void); /* reset the power ladder to active */ +void face_activity(void); +void face_status(const char *text); /* bottom status line (diag/boot) */ /* reset the power ladder to active */ /* audio.c */ void audio_init(void); @@ -41,6 +42,12 @@ bool gw_connected(void); void gw_send_text(const char *json); void gw_send_bin(const uint8_t *data, size_t len); +/* wifi_diag.c */ +void wifi_diag_start(void); + +/* c6_ota.c */ +void c6_ota_start(void); + /* desklock_main.c */ void app_on_touch(void); void app_on_playback_done(void); diff --git a/firmware/main/desklock_main.c b/firmware/main/desklock_main.c index 04c6fb2..006bd84 100644 --- a/firmware/main/desklock_main.c +++ b/firmware/main/desklock_main.c @@ -9,6 +9,9 @@ #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"; static bool s_talking; @@ -40,7 +43,9 @@ void app_on_touch(void) void app_on_playback_done(void) { - face_set(FACE_IDLE); +#if !WIFI_DIAG_MODE + face_set(FACE_IDLE); /* in diag mode the status line must stay visible */ +#endif } void app_main(void) @@ -54,7 +59,12 @@ void app_main(void) audio_init(); audio_play_gong(); +#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"); } diff --git a/firmware/main/face.c b/firmware/main/face.c index 3f4ee0f..d9e7b66 100644 --- a/firmware/main/face.c +++ b/firmware/main/face.c @@ -450,6 +450,14 @@ face_state_t face_get(void) return F.state; } +void face_status(const char *text) +{ + bsp_display_lock(UINT32_MAX); + lv_label_set_text(F.status, text); + lv_obj_clear_flag(F.status, LV_OBJ_FLAG_HIDDEN); + bsp_display_unlock(); +} + void face_activity(void) { F.last_activity = now_ms(); diff --git a/firmware/main/idf_component.yml b/firmware/main/idf_component.yml index b74b53c..ac02367 100644 --- a/firmware/main/idf_component.yml +++ b/firmware/main/idf_component.yml @@ -1,6 +1,6 @@ dependencies: idf: ">=5.4" waveshare/esp32_p4_wifi6_touch_lcd_xc: "^3.0.1" - espressif/esp_wifi_remote: "0.14.*" - espressif/esp_hosted: "1.4.*" + espressif/esp_wifi_remote: "*" + espressif/esp_hosted: "^2.12" espressif/esp_websocket_client: "~1.3.0" diff --git a/firmware/main/net.c b/firmware/main/net.c index 982df2d..7070da6 100644 --- a/firmware/main/net.c +++ b/firmware/main/net.c @@ -8,6 +8,7 @@ #include "esp_netif_sntp.h" #include "esp_wifi.h" #include "nvs_flash.h" +#include "ping/ping_sock.h" #include "desklock.h" #include "secrets.h" @@ -17,6 +18,9 @@ static const char *TAG = "net"; static esp_netif_t *s_netif; static volatile bool s_got_ip; +static void start_ping_diag(void); +static void ping_diag_task(void *arg); + /* Router DHCP has been observed to go silent for this MAC (flap throttling). * After 15 s without a lease, claim the previously granted address statically. */ static void static_ip_fallback(void *arg) @@ -40,6 +44,7 @@ static void static_ip_fallback(void *arg) esp_sntp_config_t sntp = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org"); esp_netif_sntp_init(&sntp); gw_start(); + start_ping_diag(); } vTaskDelete(NULL); } @@ -84,9 +89,72 @@ static void wifi_event(void *arg, esp_event_base_t base, int32_t id, void *data) esp_sntp_config_t sntp = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org"); esp_netif_sntp_init(&sntp); gw_start(); + start_ping_diag(); } } +static void ping_ok(esp_ping_handle_t h, void *arg) +{ + (void)arg; + uint32_t seq = 0, ms = 0; + ip_addr_t target; + esp_ping_get_profile(h, ESP_PING_PROF_SEQNO, &seq, sizeof(seq)); + esp_ping_get_profile(h, ESP_PING_PROF_TIMEGAP, &ms, sizeof(ms)); + esp_ping_get_profile(h, ESP_PING_PROF_IPADDR, &target, sizeof(target)); + ESP_LOGI(TAG, "PING %s seq=%u %ums OK", ipaddr_ntoa(&target), (unsigned)seq, (unsigned)ms); +} + +static void ping_timeout(esp_ping_handle_t h, void *arg) +{ + (void)arg; + uint32_t seq = 0; + ip_addr_t target; + esp_ping_get_profile(h, ESP_PING_PROF_SEQNO, &seq, sizeof(seq)); + esp_ping_get_profile(h, ESP_PING_PROF_IPADDR, &target, sizeof(target)); + ESP_LOGW(TAG, "PING %s seq=%u TIMEOUT", ipaddr_ntoa(&target), (unsigned)seq); +} + +static void ping_target(const char *ip_str) +{ + esp_ping_config_t pc = ESP_PING_DEFAULT_CONFIG(); + pc.count = 4; + pc.interval_ms = 500; + ipaddr_aton(ip_str, &pc.target_addr); + esp_ping_callbacks_t cbs = { + .on_ping_success = ping_ok, + .on_ping_timeout = ping_timeout, + }; + esp_ping_handle_t h; + if (esp_ping_new_session(&pc, &cbs, &h) == ESP_OK) { + esp_ping_start(h); + } +} + +static void start_ping_diag(void) +{ + static bool started; + if (!started) { + started = true; + xTaskCreate(ping_diag_task, "pingdiag", 4096, NULL, 3, NULL); + } +} + +/* boot diagnosis: can the device reach the router at all? then the server? */ +static void ping_diag_task(void *arg) +{ + (void)arg; + vTaskDelay(pdMS_TO_TICKS(3000)); + ESP_LOGI(TAG, "== ping diagnosis: router =="); + ping_target("192.168.86.1"); + vTaskDelay(pdMS_TO_TICKS(6000)); + ESP_LOGI(TAG, "== ping diagnosis: tower-of-joy =="); + ping_target("192.168.86.149"); + vTaskDelay(pdMS_TO_TICKS(6000)); + ESP_LOGI(TAG, "== slave wifi statistics dump =="); + esp_wifi_statis_dump(0xFFFFFFFF); + vTaskDelete(NULL); +} + void net_start(void) { esp_err_t err = nvs_flash_init(); diff --git a/firmware/main/wifi_diag.c b/firmware/main/wifi_diag.c new file mode 100644 index 0000000..05d7dc6 --- /dev/null +++ b/firmware/main/wifi_diag.c @@ -0,0 +1,109 @@ +/* L1 driver diagnosis: the device becomes its own access point. + * + * Ladder (docs/architecture.md): prove each layer before climbing. + * L0 SDIO control path — proven (RPC works) + * L1 wifi data path AT ALL — this file: SoftAP + DHCP + HTTP, no external network + * L2 STA to any network — phone hotspot + * L3 STA to "Outside" — the real network + * L4 gateway + Tatlock — only after L3 + * + * Join DESKLOCK-DIAG (pass desklock123) with a phone: + * - association -> log + on-screen counter + * - DHCP lease from us -> IP_EVENT_AP_STAIPASSIGNED = SDIO data path PROVEN + * - http://192.168.4.1/ -> full TCP round trip PROVEN + */ + +#include + +#include "esp_event.h" +#include "esp_http_server.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "nvs_flash.h" + +#include "desklock.h" + +static const char *TAG = "wifidiag"; + +static int s_assoc; +static int s_leases; +static int s_http_hits; + +static void show(void) +{ + char line[96]; + snprintf(line, sizeof(line), "DIAG AP: assoc %d | lease %d | http %d", + s_assoc, s_leases, s_http_hits); + face_status(line); + ESP_LOGI(TAG, "%s", line); +} + +static void diag_event(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + (void)data; + if (base == WIFI_EVENT && id == WIFI_EVENT_AP_STACONNECTED) { + s_assoc++; + ESP_LOGI(TAG, "L1a: station ASSOCIATED (radio + auth OK)"); + } else if (base == WIFI_EVENT && id == WIFI_EVENT_AP_STADISCONNECTED) { + if (s_assoc > 0) { + s_assoc--; + } + } else if (base == IP_EVENT && id == IP_EVENT_AP_STAIPASSIGNED) { + s_leases++; + ESP_LOGI(TAG, "L1b: DHCP LEASE ASSIGNED — SDIO DATA PATH PROVEN"); + } + show(); +} + +static esp_err_t hello_get(httpd_req_t *req) +{ + s_http_hits++; + ESP_LOGI(TAG, "L1c: HTTP GET — FULL TCP ROUND TRIP PROVEN"); + show(); + return httpd_resp_sendstr(req, "desklock diag: data path OK\n"); +} + +void wifi_diag_start(void) +{ + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ESP_ERROR_CHECK(nvs_flash_init()); + } + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_ap(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, diag_event, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED, diag_event, NULL)); + + wifi_config_t ap = { + .ap = { + .ssid = "DESKLOCK-DIAG", + .password = "desklock123", + .authmode = WIFI_AUTH_WPA2_PSK, + .max_connection = 4, + .channel = 6, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap)); + ESP_ERROR_CHECK(esp_wifi_start()); + + httpd_handle_t server = NULL; + httpd_config_t hcfg = HTTPD_DEFAULT_CONFIG(); + if (httpd_start(&server, &hcfg) == ESP_OK) { + static const httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = hello_get }; + httpd_register_uri_handler(server, &root); + } else { + ESP_LOGE(TAG, "httpd_start failed"); + face_status("DIAG: httpd FAILED"); + } + + ESP_LOGI(TAG, "L1 diag AP up: join DESKLOCK-DIAG / desklock123, open http://192.168.4.1/"); + face_status("DIAG: join DESKLOCK-DIAG"); +} diff --git a/firmware/sdkconfig.defaults b/firmware/sdkconfig.defaults index 916d47d..5ff2cc5 100644 --- a/firmware/sdkconfig.defaults +++ b/firmware/sdkconfig.defaults @@ -27,7 +27,31 @@ CONFIG_LV_FONT_MONTSERRAT_48=y CONFIG_LV_FONT_MONTSERRAT_28=y # ESP-Hosted C6 radio -CONFIG_ESP_WIFI_SOFTAP_SUPPORT=n +CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y # custom fonts are uncompressed, but enable the decoder as belt-and-braces CONFIG_LV_USE_FONT_COMPRESSED=y + +# ESP-Hosted board variant + wifi-remote data-path tuning (from factory brookesia config; +# without these the RPC control path works but data frames never flow) +CONFIG_SLAVE_IDF_TARGET_ESP32C6=y +CONFIG_ESP_HOSTED_CP_TARGET_ESP32C6=y +CONFIG_ESP_HOSTED_P4_DEV_BOARD_FUNC_BOARD=y +CONFIG_WIFI_RMT_STATIC_RX_BUFFER_NUM=8 +CONFIG_WIFI_RMT_DYNAMIC_RX_BUFFER_NUM=24 +CONFIG_WIFI_RMT_DYNAMIC_TX_BUFFER_NUM=24 +CONFIG_WIFI_RMT_AMPDU_TX_ENABLED=y +CONFIG_WIFI_RMT_TX_BA_WIN=16 +CONFIG_WIFI_RMT_AMPDU_RX_ENABLED=y +CONFIG_WIFI_RMT_RX_BA_WIN=16 + +# hosted task stacks MUST stay in internal RAM (PSRAM stacks assert on P4) +CONFIG_ESP_HOSTED_DFLT_TASK_FROM_SPIRAM=n + +# hosted 2.x: keep internal RAM for task stacks; buffers go to PSRAM +CONFIG_ESP_HOSTED_MEMPOOL_PREFER_SPIRAM=y +# conservative SDIO clock for first data-path proof (raise after verification) +CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ=20000 + +# headroom for diag event handlers that touch LVGL +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096