From bf7d6ff58954dfe2cd3025cb3d9cd730eac7d5f6 Mon Sep 17 00:00:00 2001 From: Paul Mokbel Date: Wed, 27 May 2026 10:08:50 -0400 Subject: [PATCH 1/3] fix: enable TinyUSB suspend/resume callback Kconfigs main/hal/storage/hal_storage.cpp:151,156 use TINYUSB_EVENT_SUSPENDED and TINYUSB_EVENT_RESUMED unconditionally: case TINYUSB_EVENT_SUSPENDED: ... case TINYUSB_EVENT_RESUMED: ... In espressif/esp_tinyusb >= 2.x those enum values are gated by CONFIG_TINYUSB_SUSPEND_CALLBACK and CONFIG_TINYUSB_RESUME_CALLBACK (see managed_components/espressif__esp_tinyusb/include/tinyusb.h). dependencies.lock does not pin esp_tinyusb, so the component manager resolves to the latest version and a fresh `idf.py build` from a clean clone fails with: error: 'TINYUSB_EVENT_SUSPENDED' was not declared in this scope error: 'TINYUSB_EVENT_RESUMED' was not declared in this scope Enable both Kconfigs in sdkconfig.defaults so the source matches the component's API surface again. --- sdkconfig.defaults | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sdkconfig.defaults b/sdkconfig.defaults index e2755ce..8a16664 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -72,6 +72,12 @@ CONFIG_WL_SECTOR_SIZE_4096=y CONFIG_TINYUSB_MSC_ENABLED=y CONFIG_TINYUSB_MSC_BUFSIZE=4096 CONFIG_TINYUSB_MSC_MOUNT_PATH="/data" +# hal_storage.cpp uses TINYUSB_EVENT_SUSPENDED / TINYUSB_EVENT_RESUMED +# unconditionally. esp_tinyusb >= 2.x gates those enum values behind these +# Kconfigs; without them the build fails with +# "TINYUSB_EVENT_SUSPENDED was not declared in this scope" +CONFIG_TINYUSB_SUSPEND_CALLBACK=y +CONFIG_TINYUSB_RESUME_CALLBACK=y # ===== I2C Bus ===== CONFIG_I2C_BUS_DYNAMIC_CONFIG=y From e87a2cbc255907cd936e31feb3dec97a963bcbdf Mon Sep 17 00:00:00 2001 From: Paul Mokbel Date: Wed, 27 May 2026 10:09:09 -0400 Subject: [PATCH 2/3] fix: bump LWIP_MAX_SOCKETS so the HTTP server can actually start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apps/app_server/app_server.cpp:1418 sets: hc.max_open_sockets = 13; ESP-IDF's httpd validates this against LWIP_MAX_SOCKETS minus 3 (three sockets are reserved internally). With the IDF default LWIP_MAX_SOCKETS=10 only 7 sockets are available, so httpd_start() fails at boot: E (...) httpd: Config option max_open_sockets is too large (max allowed 7, 3 sockets used by HTTP server internally) Either decrease this or configure LWIP_MAX_SOCKETS to a larger value E (...) app_server: httpd start fail The HTTP server then never binds port 80. Devices flashed from a clean build of this repo present an open AP that accepts associations and hands out DHCP leases, but nothing answers on 192.168.4.1 — the captive portal and the entire web UI are unreachable. The same applies on STA / LAN. The error is logged to UART (GPIO 5/4 per the current console config), which makes the symptom invisible to anyone debugging over USB-CDC. Set CONFIG_LWIP_MAX_SOCKETS=16 so the configured 13 + 3 internal fits. --- sdkconfig.defaults | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 8a16664..bf0bbd2 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -87,5 +87,15 @@ CONFIG_I2C_MS_TO_WAIT=200 CONFIG_MDNS_MULTIPLE_INSTANCE=y CONFIG_MDNS_ENABLE_CONSOLE_CLI=y +# ===== LWIP ===== +# app_server.cpp configures httpd_config_t.max_open_sockets = 13. ESP-IDF's +# httpd validates this against (LWIP_MAX_SOCKETS - 3) because three sockets +# are reserved internally by the HTTP server. The IDF default is 10, which +# leaves only 7 available, so httpd_start() returns ESP_ERR_INVALID_ARG with: +# httpd: Config option max_open_sockets is too large (max allowed 7, ...) +# The HTTP server then never binds port 80 — captive portal and LAN web UI +# are both unreachable. +CONFIG_LWIP_MAX_SOCKETS=16 + # ===== Core Dump ===== CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y From f61f77ef96eee390efb84715e5d08d7414ea0ade Mon Sep 17 00:00:00 2001 From: Paul Mokbel Date: Wed, 27 May 2026 10:09:50 -0400 Subject: [PATCH 3/3] fix: initialize mDNS once at app_server_init instead of on first AP client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mdns_init() / mdns_hostname_set() / mdns_instance_name_set() calls were inside the WiFiEvent::AP_STA_CONNECTED event handler — i.e. mDNS was only initialized when a phone joined the device's softAP. Consequences: * In STA-only operation (or any boot where no station ever joins the softAP), mDNS is never initialized and .local does not resolve on the LAN. Users get an IP from their router's DHCP but the documented papercolor.local hostname doesn't work. * The behavior is timing-dependent: as soon as one phone briefly joins the AP, mDNS comes up retroactively and starts working on STA too. This makes the bug intermittent and easy to miss. Move the init to app_server_init, right after the URI handlers are registered. The mdns component watches netif up/down events itself, so a single init at boot covers both STA and AP transitions cleanly. The existing rename path in h_wifi_config already re-calls mdns_hostname_set() when device_name changes, so renaming still works. Also add an _http._tcp service advertisement so the device shows up in Finder's Network sidebar and other Bonjour browsers without needing to know the hostname in advance. The event handler now only logs the AP_STA_CONNECTED transition. --- main/apps/app_server/app_server.cpp | 46 ++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/main/apps/app_server/app_server.cpp b/main/apps/app_server/app_server.cpp index cf662c5..6809e97 100644 --- a/main/apps/app_server/app_server.cpp +++ b/main/apps/app_server/app_server.cpp @@ -1390,28 +1390,16 @@ esp_err_t app_server_init(void) strlcpy(g_dev_state.requested_mode, hal.settings.current_mode, sizeof(g_dev_state.requested_mode)); } - /* WiFi events — additional listener, does not own the WiFi lifecycle */ + /* WiFi events — additional listener, does not own the WiFi lifecycle. */ WiFi.onEvent([](WiFiEvent ev, void *data) { switch (ev) { case WiFiEvent::STA_GOT_IP: ESP_LOGI(TAG, ">>> STA GOT IP"); hal.statusEventSend(OPERATION_EVENT_STARTUP_SUCCESS); break; - case WiFiEvent::AP_STA_CONNECTED: { + case WiFiEvent::AP_STA_CONNECTED: ESP_LOGI(TAG, ">>> AP: station connected"); - esp_err_t err = mdns_init(); - if (err) { - printf("MDNS Init failed: %d\n", err); - break; - } - char device_name[sizeof(hal.settings.device_name)] = {0}; - hal.settingsLock(); - cstring_copy(device_name, hal.settings.device_name, sizeof(device_name)); - hal.settingsUnlock(); - mdns_hostname_set(device_name); - mdns_instance_name_set("PaperColor - Config Panel"); break; - } case WiFiEvent::AP_STA_DISCONNECTED: ESP_LOGI(TAG, ">>> AP: station leaved"); break; @@ -1449,6 +1437,36 @@ esp_err_t app_server_init(void) } httpd_register_err_handler(g_srv, HTTPD_404_NOT_FOUND, http_404_error_handler); + /* Initialize mDNS once at boot so the device is reachable as + * .local on any active interface. Previously this only + * ran inside the AP_STA_CONNECTED event handler, which meant mDNS was + * only initialized when a station joined the device's softAP — so in + * STA-only operation (or before any phone ever joined the AP) + * .local never resolved on the LAN. The mdns component + * subscribes to netif up/down events itself; calling mdns_init() once + * here is enough for both STA and AP traffic, and the hostname is + * already re-applied on rename from h_wifi_config. */ + { + esp_err_t merr = mdns_init(); + if (merr != ESP_OK) { + ESP_LOGW(TAG, "mdns_init failed: %s", esp_err_to_name(merr)); + } else { + char device_name[sizeof(hal.settings.device_name)] = {0}; + hal.settingsLock(); + cstring_copy(device_name, hal.settings.device_name, sizeof(device_name)); + hal.settingsUnlock(); + if (!device_name[0]) { + cstring_copy(device_name, "papercolor", sizeof(device_name)); + } + mdns_hostname_set(device_name); + mdns_instance_name_set("PaperColor - Config Panel"); + /* Advertise an HTTP service so Bonjour-aware clients (Finder's + * Network sidebar, dns-sd -B _http._tcp, etc.) discover the + * device without needing to know its hostname. */ + mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0); + } + } + /* ==== Captive Portal Handlers ==== */ // Reference: https://github.com/espressif/esp-idf/tree/v5.5/examples/protocols/http_server/captive_portal