diff --git a/AGENTS.md b/AGENTS.md index c5c11ff..0cc4bfe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ The following matrix is authoritative for all new and refactored cross-layer con | Direction catalog, angle math, direction lookup (`domain_direction_*`) | `domain` | `components/domain/include/domain.h` | `control_plane` consumes via `domain` facade only; do not duplicate equivalent structs/enums in upper facades. | | Direction calibration policy/state model (calibration angle, sweep, search policy) | `domain` | `components/domain/include/domain.h` | Hardware wiring details must be bridged in implementation adapters, not copied into upper-layer public facades. | | Connectivity semantic prompt IDs/events (connecting/success/fail/config) | `domain` | `components/domain/include/domain.h` | `platform` must not expose duplicated semantic enums in public facade; map semantics in adapters. | +| Mini-program Wi-Fi provisioning semantics (TalkingQ BLE discovery/GATT schema, credential events, status/device-info payloads) | `domain` | `components/domain/include/domain.h` | `platform` exposes generic BLE GATT server primitives only; `control_plane` owns Wi-Fi connection policy and worker lifecycle. | | Hardware/runtime primitive descriptors (GPIO digital input config, low-level actuator primitives, storage identity/blob KV APIs, wall-clock sync API) | `platform` | `components/platform/include/platform.h` | Upper layers call through owner facade or owner-backed adapters; no semantic redefinition in non-owner public headers. | | LAN HTTP transport contract (bounded request/response structs, handler delegate, server lifecycle/state) | `platform` | `components/platform/include/platform.h` | `domain` owns route semantics and maps them in implementation; upper layers must not expose platform HTTP types. | | Multimodal session transport contract exposed to orchestration (connected/disconnected/error/text/binary event family, audio uplink callback family) | `domain` | `components/domain/include/domain.h` | `platform` internal transport types stay internal; cross-layer public contract is declared once at `domain` facade. | diff --git a/components/app_composition/src/runtime/app_composition_default_runtime.c b/components/app_composition/src/runtime/app_composition_default_runtime.c index fa06d11..5cc7e25 100644 --- a/components/app_composition/src/runtime/app_composition_default_runtime.c +++ b/components/app_composition/src/runtime/app_composition_default_runtime.c @@ -179,24 +179,33 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void) "control_plane_bootstrap_connectivity"); esp_err_t wifi_err = control_plane_bootstrap_connectivity(CONFIG_MULTIMODAL_WIFI_SSID, CONFIG_MULTIMODAL_WIFI_PASSWORD, - CONFIG_MULTIMODAL_CONNECT_TIMEOUT_MS, - "TQ-CAT"); + CONFIG_MULTIMODAL_CONNECT_TIMEOUT_MS); if (wifi_err != ESP_OK) { ESP_LOGW(TAG, "Connectivity bootstrap failed: %s", esp_err_to_name(wifi_err)); } + bool provisioning_active = control_plane_is_mini_program_provisioning_running(); app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_RUNTIME_WORKERS, "runtime_workers", - "control_plane_start_runtime_workers"); - err = control_plane_start_runtime_workers(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Runtime worker start failed: %s", esp_err_to_name(err)); - return err; + provisioning_active + ? "deferred_until_ble_provisioning_completes" + : "control_plane_start_runtime_workers"); + if (provisioning_active) { + ESP_LOGI(TAG, + "Full runtime workers deferred while BLE provisioning is active"); + } else { + err = control_plane_start_runtime_workers(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Runtime worker start failed: %s", esp_err_to_name(err)); + return err; + } } app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_NETWORK_SERVICES, "network_services", - "started_by_runtime_workers_after_readiness"); + provisioning_active + ? "deferred_with_runtime_workers" + : "started_by_runtime_workers_after_readiness"); return ESP_OK; } diff --git a/components/control_plane/include/control_plane.h b/components/control_plane/include/control_plane.h index 8abaa9f..82c2bfe 100644 --- a/components/control_plane/include/control_plane.h +++ b/components/control_plane/include/control_plane.h @@ -119,15 +119,16 @@ esp_err_t control_plane_calibrate(void); * Semantics: * - `connect_timeout_ms <= 0`: apply internal default timeout. * - Operation is safe to call repeatedly; existing good state is reused when possible. - * - Returns `ESP_OK` when STA is connected or a fallback provisioning path is started. + * - Returns `ESP_OK` when STA is connected or mini-program BLE provisioning is active. * - On timeout/failure, function returns explicit error and does not continue hidden * destructive retries after return. */ esp_err_t control_plane_bootstrap_connectivity(const char *seed_ssid, const char *seed_password, - int connect_timeout_ms, - const char *config_ap_ssid_prefix); + int connect_timeout_ms); bool control_plane_is_sta_connected(void); +esp_err_t control_plane_stop_mini_program_provisioning(int timeout_ms); +bool control_plane_is_mini_program_provisioning_running(void); /** * @brief Execute the Plan A boot sound policy with bounded caller semantics. diff --git a/components/control_plane/internal/control_plane_internal.h b/components/control_plane/internal/control_plane_internal.h index 01933ed..3f7d62d 100644 --- a/components/control_plane/internal/control_plane_internal.h +++ b/components/control_plane/internal/control_plane_internal.h @@ -155,6 +155,7 @@ esp_err_t control_plane_start_key_input_task(void); void control_plane_stop_key_input_task(void); esp_err_t control_plane_schedule_websocket_wait_task(void); void control_plane_set_ptt_active(bool active); +esp_err_t control_plane_enter_mini_program_reprovisioning(const char *reason); esp_err_t control_plane_remote_log_start(void); void control_plane_remote_log_stop(int timeout_ms); diff --git a/components/control_plane/src/runtime/control_plane_connectivity_runtime.c b/components/control_plane/src/runtime/control_plane_connectivity_runtime.c index ceffba5..b40b8f3 100644 --- a/components/control_plane/src/runtime/control_plane_connectivity_runtime.c +++ b/components/control_plane/src/runtime/control_plane_connectivity_runtime.c @@ -6,9 +6,13 @@ #include "control_plane.h" +#include + #include "control_plane_internal.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/queue.h" #include "freertos/task.h" #include "sdkconfig.h" @@ -24,13 +28,33 @@ #define CONFIG_TQ_BOOT_SOUND_FILE "" #endif +#ifndef CONFIG_TQ_MINI_PROGRAM_PROVISIONING_ENABLE +#define CONFIG_TQ_MINI_PROGRAM_PROVISIONING_ENABLE 0 +#endif + +#ifndef CONFIG_TQ_MINI_PROGRAM_PROVISIONING_CONNECT_TIMEOUT_MS +#define CONFIG_TQ_MINI_PROGRAM_PROVISIONING_CONNECT_TIMEOUT_MS 30000 +#endif + #define CONTROL_PLANE_WIFI_SUCCESS_PROMPT_FILE "/littlefs/audio_prompts/network_connect_success.opus" #define CONTROL_PLANE_STA_CONNECT_MAX_ATTEMPTS 3 #define CONTROL_PLANE_STA_RETRY_BASE_DELAY_MS 300 +#define CONTROL_PLANE_PROVISIONING_QUEUE_LENGTH 1 +#define CONTROL_PLANE_PROVISIONING_TASK_STACK_WORDS 4096 +#define CONTROL_PLANE_PROVISIONING_TASK_PRIORITY 5 +#define CONTROL_PLANE_PROVISIONING_STOP_REQUESTED_BIT BIT0 +#define CONTROL_PLANE_PROVISIONING_WORKER_DONE_BIT BIT1 +#define CONTROL_PLANE_PROVISIONING_POLL_MS 100 +#define CONTROL_PLANE_PROVISIONING_NOTIFY_SETTLE_MS 750 +#define CONTROL_PLANE_PROVISIONING_TRANSPORT_STOP_MS 2000 static const char *TAG = "control_plane_rt"; static bool s_wifi_success_prompt_played; +static portMUX_TYPE s_provisioning_lock = portMUX_INITIALIZER_UNLOCKED; +static QueueHandle_t s_provisioning_queue; +static EventGroupHandle_t s_provisioning_events; +static TaskHandle_t s_provisioning_task; static int elapsed_ms_since(TickType_t start_tick) { @@ -159,11 +183,6 @@ static esp_err_t control_plane_connect_sta_with_feedback(int timeout_ms, return ESP_OK; } -static bool control_plane_wifi_connect_error_keeps_credentials(esp_err_t err) -{ - return domain_runtime_wifi_connect_error_keeps_credentials(err); -} - static void control_plane_register_wifi_behavior_hooks(void) { domain_wifi_behavior_hooks_t hooks = { @@ -174,10 +193,261 @@ static void control_plane_register_wifi_behavior_hooks(void) domain_runtime_wifi_portal_set_behavior_hooks(&hooks); } +static void control_plane_provisioning_notify(domain_wifi_provisioning_status_t status) +{ + esp_err_t err = domain_runtime_mini_program_provisioning_notify(status); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Mini-program provisioning status notify failed: %s", + esp_err_to_name(err)); + } +} + +static void control_plane_finish_provisioning_ui(void) +{ + const domain_expression_request_t idle_request = { + .id = DOMAIN_EXPRESSION_IDLE, + .trigger = DOMAIN_EXPRESSION_TRIGGER_WIFI_RECONNECTED, + }; + esp_err_t err = domain_runtime_request_expression(&idle_request); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, + "Restore idle expression after provisioning failed: %s", + esp_err_to_name(err)); + } +} + +static void control_plane_provisioning_event_callback( + const domain_wifi_provisioning_event_t *event, + void *user_data) +{ + (void)user_data; + if (event == NULL) { + return; + } + + QueueHandle_t queue; + portENTER_CRITICAL(&s_provisioning_lock); + queue = s_provisioning_queue; + portEXIT_CRITICAL(&s_provisioning_lock); + if (queue == NULL || xQueueOverwrite(queue, event) != pdTRUE) { + ESP_LOGW(TAG, "Drop mini-program provisioning event: queue unavailable"); + } +} + +static bool control_plane_provisioning_stop_requested(void) +{ + return s_provisioning_events != NULL && + (xEventGroupGetBits(s_provisioning_events) & + CONTROL_PLANE_PROVISIONING_STOP_REQUESTED_BIT) != 0; +} + +static void control_plane_provisioning_worker(void *arg) +{ + (void)arg; + bool provisioning_succeeded = false; + + while (!control_plane_provisioning_stop_requested()) { + domain_wifi_provisioning_event_t event = {0}; + if (xQueueReceive(s_provisioning_queue, + &event, + pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_POLL_MS)) != pdTRUE) { + continue; + } + + if (event.type == DOMAIN_WIFI_PROVISIONING_EVENT_BAD_JSON) { + control_plane_provisioning_notify( + DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_BAD_JSON); + continue; + } + if (event.type == DOMAIN_WIFI_PROVISIONING_EVENT_EMPTY_SSID) { + control_plane_provisioning_notify( + DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_EMPTY_SSID); + continue; + } + if (event.type != DOMAIN_WIFI_PROVISIONING_EVENT_CREDENTIALS) { + continue; + } + + control_plane_provisioning_notify( + DOMAIN_WIFI_PROVISIONING_STATUS_SAVING_WIFI); + control_plane_provisioning_notify( + DOMAIN_WIFI_PROVISIONING_STATUS_WIFI_CONNECTING); + esp_err_t err = domain_runtime_wifi_portal_connect_sta_with_credentials( + event.credentials.ssid, + event.credentials.password, + CONFIG_TQ_MINI_PROGRAM_PROVISIONING_CONNECT_TIMEOUT_MS); + memset(&event, 0, sizeof(event)); + if (err != ESP_OK) { + control_plane_wifi_set_keep_awake(true); + control_plane_provisioning_notify( + err == ESP_ERR_TIMEOUT + ? DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_TIMEOUT + : DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_CONNECT); + continue; + } + + control_plane_play_wifi_success_prompt(); + control_plane_provisioning_notify( + DOMAIN_WIFI_PROVISIONING_STATUS_WIFI_GOT_IP); + control_plane_provisioning_notify( + DOMAIN_WIFI_PROVISIONING_STATUS_READY); + vTaskDelay(pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_NOTIFY_SETTLE_MS)); + provisioning_succeeded = true; + break; + } + + esp_err_t stop_err = domain_runtime_mini_program_provisioning_stop( + CONTROL_PLANE_PROVISIONING_TRANSPORT_STOP_MS); + if (stop_err != ESP_OK) { + ESP_LOGE(TAG, "Mini-program BLE teardown failed: %s", + esp_err_to_name(stop_err)); + } + control_plane_wifi_set_keep_awake(false); + + portENTER_CRITICAL(&s_provisioning_lock); + s_provisioning_task = NULL; + portEXIT_CRITICAL(&s_provisioning_lock); + (void)xEventGroupSetBits(s_provisioning_events, + CONTROL_PLANE_PROVISIONING_WORKER_DONE_BIT); + + if (provisioning_succeeded && stop_err == ESP_OK) { + control_plane_finish_provisioning_ui(); + ESP_LOGI(TAG, + "BLE provisioning completed; starting deferred runtime workers"); + esp_err_t runtime_err = control_plane_start_runtime_workers(); + if (runtime_err != ESP_OK) { + ESP_LOGE(TAG, + "Deferred runtime worker start failed: %s", + esp_err_to_name(runtime_err)); + } + } else if (provisioning_succeeded) { + ESP_LOGE(TAG, + "Runtime workers remain deferred because BLE teardown did not complete"); + } + vTaskDelete(NULL); +} + +static esp_err_t control_plane_start_mini_program_provisioning(void) +{ +#if !CONFIG_TQ_MINI_PROGRAM_PROVISIONING_ENABLE + return ESP_ERR_NOT_SUPPORTED; +#else + portENTER_CRITICAL(&s_provisioning_lock); + bool already_running = s_provisioning_task != NULL; + portEXIT_CRITICAL(&s_provisioning_lock); + if (already_running) { + return ESP_OK; + } + + if (s_provisioning_queue == NULL) { + s_provisioning_queue = xQueueCreate( + CONTROL_PLANE_PROVISIONING_QUEUE_LENGTH, + sizeof(domain_wifi_provisioning_event_t)); + } + if (s_provisioning_events == NULL) { + s_provisioning_events = xEventGroupCreate(); + } + if (s_provisioning_queue == NULL || s_provisioning_events == NULL) { + return ESP_ERR_NO_MEM; + } + (void)xQueueReset(s_provisioning_queue); + (void)xEventGroupClearBits( + s_provisioning_events, + CONTROL_PLANE_PROVISIONING_STOP_REQUESTED_BIT | + CONTROL_PLANE_PROVISIONING_WORKER_DONE_BIT); + + BaseType_t created = xTaskCreate(control_plane_provisioning_worker, + "mini_prog_wifi", + CONTROL_PLANE_PROVISIONING_TASK_STACK_WORDS, + NULL, + CONTROL_PLANE_PROVISIONING_TASK_PRIORITY, + &s_provisioning_task); + if (created != pdPASS) { + s_provisioning_task = NULL; + return ESP_ERR_NO_MEM; + } + + esp_err_t err = domain_runtime_mini_program_provisioning_start( + control_plane_provisioning_event_callback, + NULL); + if (err != ESP_OK) { + (void)xEventGroupSetBits(s_provisioning_events, + CONTROL_PLANE_PROVISIONING_STOP_REQUESTED_BIT); + (void)xEventGroupWaitBits( + s_provisioning_events, + CONTROL_PLANE_PROVISIONING_WORKER_DONE_BIT, + pdFALSE, + pdTRUE, + pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_TRANSPORT_STOP_MS)); + return err; + } + ESP_LOGI(TAG, "Mini-program Wi-Fi provisioning started"); + return ESP_OK; +#endif +} + +bool control_plane_is_mini_program_provisioning_running(void) +{ + bool running; + portENTER_CRITICAL(&s_provisioning_lock); + running = s_provisioning_task != NULL; + portEXIT_CRITICAL(&s_provisioning_lock); + return running; +} + +esp_err_t control_plane_stop_mini_program_provisioning(int timeout_ms) +{ + if (timeout_ms < 0) { + return ESP_ERR_INVALID_ARG; + } + if (s_provisioning_events == NULL || + !control_plane_is_mini_program_provisioning_running()) { + return domain_runtime_mini_program_provisioning_stop(timeout_ms); + } + + (void)xEventGroupSetBits(s_provisioning_events, + CONTROL_PLANE_PROVISIONING_STOP_REQUESTED_BIT); + esp_err_t transport_err = domain_runtime_mini_program_provisioning_stop(0); + if (timeout_ms == 0) { + return transport_err; + } + EventBits_t bits = xEventGroupWaitBits( + s_provisioning_events, + CONTROL_PLANE_PROVISIONING_WORKER_DONE_BIT, + pdFALSE, + pdTRUE, + pdMS_TO_TICKS(timeout_ms)); + return (bits & CONTROL_PLANE_PROVISIONING_WORKER_DONE_BIT) != 0 + ? transport_err + : ESP_ERR_TIMEOUT; +} + +esp_err_t control_plane_enter_mini_program_reprovisioning(const char *reason) +{ +#if !CONFIG_TQ_MINI_PROGRAM_PROVISIONING_ENABLE + (void)reason; + return ESP_ERR_NOT_SUPPORTED; +#else + if (control_plane_is_mini_program_provisioning_running()) { + return ESP_OK; + } + ESP_LOGW(TAG, "Entering mini-program BLE reprovisioning: reason=%s", + reason != NULL ? reason : "unspecified"); + control_plane_wifi_set_keep_awake(true); + esp_err_t err = domain_runtime_wifi_portal_stop_sta(); + if (err == ESP_OK) { + err = control_plane_start_mini_program_provisioning(); + } + if (err != ESP_OK) { + control_plane_wifi_set_keep_awake(false); + } + return err; +#endif +} + esp_err_t control_plane_bootstrap_connectivity(const char *seed_ssid, const char *seed_password, - int connect_timeout_ms, - const char *config_ap_ssid_prefix) + int connect_timeout_ms) { ESP_LOGI(TAG, "Boot Wi-Fi config SSID=%s timeout_ms=%d", @@ -196,24 +466,27 @@ esp_err_t control_plane_bootstrap_connectivity(const char *seed_ssid, if (err == ESP_OK) { return ESP_OK; } - if (control_plane_wifi_connect_error_keeps_credentials(err)) { - ESP_LOGW(TAG, - "STA connect failed, keep credentials for runtime retry: %s", - esp_err_to_name(err)); - } else { - ESP_LOGW(TAG, "STA connect failed, clear credentials and fallback: %s", - esp_err_to_name(err)); - (void)domain_runtime_wifi_portal_clear_saved_credentials(); - } + ESP_LOGW(TAG, + "Boot STA connect failed; preserve credentials and enter BLE provisioning immediately: %s", + esp_err_to_name(err)); + } + + const domain_expression_request_t connecting_request = { + .id = DOMAIN_EXPRESSION_WIFI_CONNECTING, + .trigger = DOMAIN_EXPRESSION_TRIGGER_WIFI_BOOTSTRAP, + }; + esp_err_t display_err = domain_runtime_request_expression(&connecting_request); + if (display_err != ESP_OK && display_err != ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, "Wi-Fi connecting QR display unavailable: %s", + esp_err_to_name(display_err)); } control_plane_wifi_set_keep_awake(true); - err = domain_runtime_wifi_portal_start_config_ap( - (config_ap_ssid_prefix != NULL && config_ap_ssid_prefix[0] != '\0') - ? config_ap_ssid_prefix - : "TQ-CAT"); + err = control_plane_start_mini_program_provisioning(); if (err != ESP_OK) { control_plane_wifi_set_keep_awake(false); + ESP_LOGE(TAG, "BLE provisioning unavailable; SoftAP fallback disabled: %s", + esp_err_to_name(err)); } return err; } diff --git a/components/control_plane/src/runtime/control_plane_runtime_workers.c b/components/control_plane/src/runtime/control_plane_runtime_workers.c index eebfec1..47a29f7 100644 --- a/components/control_plane/src/runtime/control_plane_runtime_workers.c +++ b/components/control_plane/src/runtime/control_plane_runtime_workers.c @@ -110,6 +110,14 @@ #define CONFIG_TQ_NETWORK_AUTO_REASSOCIATION_ENABLE 0 #endif +#ifndef CONFIG_TQ_MINI_PROGRAM_PROVISIONING_RECONNECT_FAILURES +#define CONFIG_TQ_MINI_PROGRAM_PROVISIONING_RECONNECT_FAILURES 2 +#endif + +#ifndef CONFIG_TQ_MINI_PROGRAM_PROVISIONING_ENABLE +#define CONFIG_TQ_MINI_PROGRAM_PROVISIONING_ENABLE 0 +#endif + #if CONFIG_TQ_NETWORK_AUTO_REASSOCIATION_ENABLE #define CONTROL_PLANE_NETWORK_FAILURE_REASSOC_THRESHOLD 1U #endif @@ -1708,6 +1716,7 @@ static void control_plane_wifi_state_task(void *arg) TickType_t last_ws_start_attempt_tick = 0; uint32_t reconnect_delay_ms = CONTROL_PLANE_WIFI_RECONNECT_BASE_DELAY_MS; bool reconnect_attempted = false; + uint32_t reconnect_failure_count = 0; uint8_t ptt_wifi_foreground_attempts = 0; #if CONFIG_TQ_MEMORY_MONITOR_ENABLE @@ -1715,10 +1724,15 @@ static void control_plane_wifi_state_task(void *arg) #endif if (!was_connected) { - ESP_LOGI(TAG, "Wi-Fi STA unavailable at runtime start; requesting disconnected expression"); - control_plane_request_wifi_state_expression(DOMAIN_EXPRESSION_WIFI_DISCONNECTED, - DOMAIN_EXPRESSION_TRIGGER_WIFI_DISCONNECTED, - "Wi-Fi disconnected"); + bool provisioning_running = + control_plane_is_mini_program_provisioning_running(); + ESP_LOGI(TAG, "Wi-Fi STA unavailable at runtime start; requesting offline expression"); + control_plane_request_wifi_state_expression( + provisioning_running ? DOMAIN_EXPRESSION_WIFI_CONNECTING + : DOMAIN_EXPRESSION_WIFI_DISCONNECTED, + provisioning_running ? DOMAIN_EXPRESSION_TRIGGER_WIFI_BOOTSTRAP + : DOMAIN_EXPRESSION_TRIGGER_WIFI_DISCONNECTED, + provisioning_running ? "Wi-Fi provisioning" : "Wi-Fi disconnected"); disconnected_expression_active = true; last_disconnected_request_tick = xTaskGetTickCount(); } else { @@ -1774,9 +1788,14 @@ static void control_plane_wifi_state_task(void *arg) if (was_connected || !disconnected_expression_active) { ESP_LOGI(TAG, "Wi-Fi STA disconnected; requesting disconnected expression"); } - control_plane_request_wifi_state_expression(DOMAIN_EXPRESSION_WIFI_DISCONNECTED, - DOMAIN_EXPRESSION_TRIGGER_WIFI_DISCONNECTED, - "Wi-Fi disconnected"); + bool provisioning_running = + control_plane_is_mini_program_provisioning_running(); + control_plane_request_wifi_state_expression( + provisioning_running ? DOMAIN_EXPRESSION_WIFI_CONNECTING + : DOMAIN_EXPRESSION_WIFI_DISCONNECTED, + provisioning_running ? DOMAIN_EXPRESSION_TRIGGER_WIFI_BOOTSTRAP + : DOMAIN_EXPRESSION_TRIGGER_WIFI_DISCONNECTED, + provisioning_running ? "Wi-Fi provisioning" : "Wi-Fi disconnected"); disconnected_expression_active = true; last_disconnected_request_tick = now_tick; } @@ -1791,7 +1810,11 @@ static void control_plane_wifi_state_task(void *arg) bool reconnect_due = ptt_foreground || !reconnect_attempted || (now_tick - last_reconnect_attempt_tick) >= reconnect_delay_ticks; - if (reconnect_due && domain_runtime_wifi_portal_has_saved_credentials()) { + bool provisioning_running = + control_plane_is_mini_program_provisioning_running(); + bool has_saved_credentials = + domain_runtime_wifi_portal_has_saved_credentials(); + if (reconnect_due && !provisioning_running && has_saved_credentials) { esp_err_t reconnect_err = control_plane_attempt_wifi_reconnect(); now_tick = xTaskGetTickCount(); reconnect_attempted = true; @@ -1802,11 +1825,37 @@ static void control_plane_wifi_state_task(void *arg) if (reconnect_err == ESP_OK) { connected = true; reconnect_delay_ms = CONTROL_PLANE_WIFI_RECONNECT_BASE_DELAY_MS; + reconnect_failure_count = 0; ptt_wifi_foreground_attempts = 0; } else { + if (reconnect_failure_count < UINT32_MAX) { + reconnect_failure_count++; + } reconnect_delay_ms = control_plane_next_wifi_reconnect_delay_ms(reconnect_delay_ms); } } + if (!connected && + CONFIG_TQ_MINI_PROGRAM_PROVISIONING_ENABLE && + !control_plane_get_ptt_active() && + !control_plane_is_mini_program_provisioning_running() && + (!has_saved_credentials || + reconnect_failure_count >= + CONFIG_TQ_MINI_PROGRAM_PROVISIONING_RECONNECT_FAILURES)) { + esp_err_t provisioning_err = + control_plane_enter_mini_program_reprovisioning( + has_saved_credentials + ? "wifi_reconnect_failed" + : "wifi_credentials_missing"); + if (provisioning_err == ESP_OK) { + reconnect_failure_count = 0; + reconnect_delay_ms = CONTROL_PLANE_WIFI_RECONNECT_BASE_DELAY_MS; + reconnect_attempted = false; + disconnected_expression_active = false; + } else { + ESP_LOGW(TAG, "Mini-program reprovisioning failed: %s", + esp_err_to_name(provisioning_err)); + } + } if (!control_plane_get_ptt_active()) { ptt_wifi_foreground_attempts = 0; } @@ -1840,6 +1889,7 @@ static void control_plane_wifi_state_task(void *arg) disconnect_started_tick = 0; reconnect_delay_ms = CONTROL_PLANE_WIFI_RECONNECT_BASE_DELAY_MS; reconnect_attempted = false; + reconnect_failure_count = 0; } if (connected && (!was_connected || disconnected_expression_active)) { @@ -1859,6 +1909,7 @@ static void control_plane_wifi_state_task(void *arg) } else if (connected) { disconnect_pending = false; disconnect_started_tick = 0; + reconnect_failure_count = 0; } if (connected) { @@ -2341,6 +2392,8 @@ void control_plane_stop_event_observer_runtime(void) void control_plane_stop_runtime_workers(void) { control_plane_set_runtime_stop_requested(true); + (void)control_plane_stop_mini_program_provisioning( + CONTROL_PLANE_TASK_STOP_WAIT_MS); control_plane_stop_user_config_server(); (void)control_plane_local_wake_stop(CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS); diff --git a/components/domain/CMakeLists.txt b/components/domain/CMakeLists.txt index d27048f..832eceb 100644 --- a/components/domain/CMakeLists.txt +++ b/components/domain/CMakeLists.txt @@ -9,6 +9,7 @@ idf_component_register( "src/fortune/domain_user_config_server.c" "src/runtime/domain_runtime_adapters.c" "src/runtime/domain_local_command.c" + "src/runtime/domain_mini_program_provisioning.c" INCLUDE_DIRS "include" PRIV_INCLUDE_DIRS diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index 9e8da07..4888a45 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -113,6 +113,7 @@ typedef enum { DOMAIN_EXPRESSION_IDLE = 0, DOMAIN_EXPRESSION_SERVER_ERROR, DOMAIN_EXPRESSION_SLOT_REELS, + DOMAIN_EXPRESSION_WIFI_CONNECTING, DOMAIN_EXPRESSION_WIFI_DISCONNECTED, DOMAIN_EXPRESSION_WEALTH_SPIN, DOMAIN_EXPRESSION_WEALTH_DIRECTION, @@ -127,6 +128,7 @@ typedef enum { DOMAIN_EXPRESSION_TRIGGER_SERVER_TASK_FAILED, DOMAIN_EXPRESSION_TRIGGER_SERVER_OUTPUT_ERROR, DOMAIN_EXPRESSION_TRIGGER_SLOT_SWITCH, + DOMAIN_EXPRESSION_TRIGGER_WIFI_BOOTSTRAP, DOMAIN_EXPRESSION_TRIGGER_WIFI_DISCONNECTED, DOMAIN_EXPRESSION_TRIGGER_WIFI_RECONNECTED, DOMAIN_EXPRESSION_TRIGGER_MODEL_DIRECTION, @@ -430,11 +432,64 @@ bool domain_runtime_wifi_connect_error_keeps_credentials(esp_err_t err); * - Returns explicit failure on timeout/connection error. */ esp_err_t domain_runtime_wifi_portal_connect_sta(int timeout_ms); +/** + * @brief Validate candidate credentials before replacing persisted Wi-Fi state. + * + * The previous credentials remain available when the bounded connection trial + * fails. A successful trial commits only the new credentials. + */ +esp_err_t domain_runtime_wifi_portal_connect_sta_with_credentials( + const char *ssid, + const char *password, + int timeout_ms); esp_err_t domain_runtime_wifi_portal_reconnect_sta(int timeout_ms); +esp_err_t domain_runtime_wifi_portal_stop_sta(void); bool domain_runtime_is_network_connect_error(esp_err_t err); esp_err_t domain_runtime_wifi_portal_save_credentials(const char *ssid, const char *password); esp_err_t domain_runtime_wifi_portal_clear_saved_credentials(void); +#define DOMAIN_WIFI_PROVISIONING_SSID_MAX_LEN 33 +#define DOMAIN_WIFI_PROVISIONING_PASSWORD_MAX_LEN 65 + +typedef struct { + char ssid[DOMAIN_WIFI_PROVISIONING_SSID_MAX_LEN]; + char password[DOMAIN_WIFI_PROVISIONING_PASSWORD_MAX_LEN]; +} domain_wifi_provisioning_credentials_t; + +typedef enum { + DOMAIN_WIFI_PROVISIONING_EVENT_CREDENTIALS = 0, + DOMAIN_WIFI_PROVISIONING_EVENT_BAD_JSON, + DOMAIN_WIFI_PROVISIONING_EVENT_EMPTY_SSID, +} domain_wifi_provisioning_event_type_t; + +typedef struct { + domain_wifi_provisioning_event_type_t type; + domain_wifi_provisioning_credentials_t credentials; +} domain_wifi_provisioning_event_t; + +typedef void (*domain_wifi_provisioning_event_callback_t)( + const domain_wifi_provisioning_event_t *event, + void *user_data); + +typedef enum { + DOMAIN_WIFI_PROVISIONING_STATUS_WIFI_CONNECTING = 0, + DOMAIN_WIFI_PROVISIONING_STATUS_WIFI_GOT_IP, + DOMAIN_WIFI_PROVISIONING_STATUS_SAVING_WIFI, + DOMAIN_WIFI_PROVISIONING_STATUS_READY, + DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_BAD_JSON, + DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_EMPTY_SSID, + DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_TIMEOUT, + DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_CONNECT, +} domain_wifi_provisioning_status_t; + +/** Start the fixed TalkingQ mini-program BLE provisioning protocol. */ +esp_err_t domain_runtime_mini_program_provisioning_start( + domain_wifi_provisioning_event_callback_t callback, + void *user_data); +esp_err_t domain_runtime_mini_program_provisioning_notify( + domain_wifi_provisioning_status_t status); +esp_err_t domain_runtime_mini_program_provisioning_stop(int timeout_ms); + esp_err_t domain_runtime_init_lcd_device(void); void domain_runtime_start_lcd_render_loop(void); /** diff --git a/components/domain/src/runtime/domain_mini_program_provisioning.c b/components/domain/src/runtime/domain_mini_program_provisioning.c new file mode 100644 index 0000000..e5d9ed9 --- /dev/null +++ b/components/domain/src/runtime/domain_mini_program_provisioning.c @@ -0,0 +1,410 @@ +/* + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "domain.h" + +#include +#include + +#include "cJSON.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "platform.h" + +#define DOMAIN_PROVISIONING_ID_MAX_LEN 64 +#define DOMAIN_PROVISIONING_SERIAL_MAX_LEN 64 +#define DOMAIN_PROVISIONING_VERSION_MAX_LEN 32 +#define DOMAIN_PROVISIONING_INFO_MAX_LEN 384 +#define DOMAIN_PROVISIONING_NAME_PREFIX "TalkingQ-LuckyCat-" +#define DOMAIN_PROVISIONING_DEVICE_NAME "TalkingQ Lucky Cat" +#define DOMAIN_PROVISIONING_WRITE_TIMEOUT_MS 3000 + +typedef enum { + DOMAIN_PROVISIONING_WRITE_INCOMPLETE = 0, + DOMAIN_PROVISIONING_WRITE_COMPLETE, + DOMAIN_PROVISIONING_WRITE_INVALID, +} domain_provisioning_write_result_t; + +static const char *TAG = "domain_provisioning"; +static portMUX_TYPE s_callback_lock = portMUX_INITIALIZER_UNLOCKED; +static domain_wifi_provisioning_event_callback_t s_event_callback; +static void *s_event_user_data; +static portMUX_TYPE s_write_lock = portMUX_INITIALIZER_UNLOCKED; +static uint8_t s_write_buffer[PLATFORM_BLE_GATT_VALUE_MAX_LEN]; +static char s_complete_write[PLATFORM_BLE_GATT_VALUE_MAX_LEN + 1]; +static size_t s_write_buffer_len; +static TickType_t s_write_last_tick; + +/* NimBLE UUID128 storage uses the little-endian byte order expected by the host. */ +/* 4fafc201-1fb5-459e-8fcc-c5c9c331914b */ +static const platform_ble_uuid128_t s_service_uuid = { + .bytes = {0x4b, 0x91, 0x31, 0xc3, 0xc9, 0xc5, 0xcc, 0x8f, + 0x9e, 0x45, 0xb5, 0x1f, 0x01, 0xc2, 0xaf, 0x4f}, +}; +/* beb5483e-36e1-4688-b7f5-ea07361b26a8 */ +static const platform_ble_uuid128_t s_wifi_write_uuid = { + .bytes = {0xa8, 0x26, 0x1b, 0x36, 0x07, 0xea, 0xf5, 0xb7, + 0x88, 0x46, 0xe1, 0x36, 0x3e, 0x48, 0xb5, 0xbe}, +}; +/* beb5483e-36e1-4688-b7f5-ea07361b26a9 */ +static const platform_ble_uuid128_t s_status_uuid = { + .bytes = {0xa9, 0x26, 0x1b, 0x36, 0x07, 0xea, 0xf5, 0xb7, + 0x88, 0x46, 0xe1, 0x36, 0x3e, 0x48, 0xb5, 0xbe}, +}; +/* beb5483e-36e1-4688-b7f5-ea07361b26aa */ +static const platform_ble_uuid128_t s_device_info_uuid = { + .bytes = {0xaa, 0x26, 0x1b, 0x36, 0x07, 0xea, 0xf5, 0xb7, + 0x88, 0x46, 0xe1, 0x36, 0x3e, 0x48, 0xb5, 0xbe}, +}; + +static void domain_provisioning_dispatch_event( + const domain_wifi_provisioning_event_t *event) +{ + domain_wifi_provisioning_event_callback_t callback; + void *user_data; + + portENTER_CRITICAL(&s_callback_lock); + callback = s_event_callback; + user_data = s_event_user_data; + portEXIT_CRITICAL(&s_callback_lock); + + if (callback != NULL) { + callback(event, user_data); + } +} + +static void domain_provisioning_reset_write_buffer(void) +{ + portENTER_CRITICAL(&s_write_lock); + memset(s_write_buffer, 0, sizeof(s_write_buffer)); + memset(s_complete_write, 0, sizeof(s_complete_write)); + s_write_buffer_len = 0U; + s_write_last_tick = 0; + portEXIT_CRITICAL(&s_write_lock); +} + +static domain_provisioning_write_result_t +domain_provisioning_append_write_fragment(const uint8_t *data, + size_t data_len, + size_t *out_buffered_len) +{ + if (data == NULL || data_len == 0U) { + return DOMAIN_PROVISIONING_WRITE_INVALID; + } + + TickType_t now = xTaskGetTickCount(); + TickType_t stale_ticks = pdMS_TO_TICKS(DOMAIN_PROVISIONING_WRITE_TIMEOUT_MS); + domain_provisioning_write_result_t result = DOMAIN_PROVISIONING_WRITE_INCOMPLETE; + + portENTER_CRITICAL(&s_write_lock); + if (s_write_buffer_len > 0U && + (now - s_write_last_tick) > stale_ticks) { + s_write_buffer_len = 0U; + } + if (data_len > sizeof(s_write_buffer) - s_write_buffer_len) { + s_write_buffer_len = 0U; + result = DOMAIN_PROVISIONING_WRITE_INVALID; + } else { + memcpy(s_write_buffer + s_write_buffer_len, data, data_len); + s_write_buffer_len += data_len; + s_write_last_tick = now; + + bool saw_object = false; + bool in_string = false; + bool escaped = false; + int depth = 0; + for (size_t i = 0U; i < s_write_buffer_len; ++i) { + uint8_t value = s_write_buffer[i]; + if (!saw_object) { + if (value == ' ' || value == '\t' || value == '\r' || value == '\n') { + continue; + } + if (value != '{') { + result = DOMAIN_PROVISIONING_WRITE_INVALID; + break; + } + saw_object = true; + depth = 1; + continue; + } + if (in_string) { + if (escaped) { + escaped = false; + } else if (value == '\\') { + escaped = true; + } else if (value == '"') { + in_string = false; + } + continue; + } + if (value == '"') { + in_string = true; + } else if (value == '{') { + depth++; + } else if (value == '}') { + depth--; + if (depth < 0) { + result = DOMAIN_PROVISIONING_WRITE_INVALID; + break; + } + if (depth == 0) { + result = DOMAIN_PROVISIONING_WRITE_COMPLETE; + for (size_t trailing = i + 1U; + trailing < s_write_buffer_len; + ++trailing) { + uint8_t trailing_value = s_write_buffer[trailing]; + if (trailing_value != ' ' && trailing_value != '\t' && + trailing_value != '\r' && trailing_value != '\n') { + result = DOMAIN_PROVISIONING_WRITE_INVALID; + break; + } + } + break; + } + } + } + + if (result == DOMAIN_PROVISIONING_WRITE_COMPLETE) { + memcpy(s_complete_write, s_write_buffer, s_write_buffer_len); + s_complete_write[s_write_buffer_len] = '\0'; + } + } + + if (out_buffered_len != NULL) { + *out_buffered_len = s_write_buffer_len; + } + if (result != DOMAIN_PROVISIONING_WRITE_INCOMPLETE) { + memset(s_write_buffer, 0, s_write_buffer_len); + s_write_buffer_len = 0U; + s_write_last_tick = 0; + } + portEXIT_CRITICAL(&s_write_lock); + return result; +} + +static void domain_provisioning_process_complete_write(const char *json, + size_t data_len) +{ + domain_wifi_provisioning_event_t event = { + .type = DOMAIN_WIFI_PROVISIONING_EVENT_BAD_JSON, + }; + + cJSON *root = cJSON_ParseWithLength(json, data_len); + if (root == NULL || !cJSON_IsObject(root)) { + cJSON_Delete(root); + domain_provisioning_dispatch_event(&event); + return; + } + + const cJSON *ssid = cJSON_GetObjectItemCaseSensitive(root, "ssid"); + const cJSON *password = cJSON_GetObjectItemCaseSensitive(root, "password"); + if (!cJSON_IsString(ssid) || ssid->valuestring == NULL || + !cJSON_IsString(password) || password->valuestring == NULL || + strlen(ssid->valuestring) >= sizeof(event.credentials.ssid) || + strlen(password->valuestring) >= sizeof(event.credentials.password)) { + cJSON_Delete(root); + domain_provisioning_dispatch_event(&event); + return; + } + if (ssid->valuestring[0] == '\0') { + event.type = DOMAIN_WIFI_PROVISIONING_EVENT_EMPTY_SSID; + cJSON_Delete(root); + domain_provisioning_dispatch_event(&event); + return; + } + + event.type = DOMAIN_WIFI_PROVISIONING_EVENT_CREDENTIALS; + strlcpy(event.credentials.ssid, + ssid->valuestring, + sizeof(event.credentials.ssid)); + strlcpy(event.credentials.password, + password->valuestring, + sizeof(event.credentials.password)); + cJSON_Delete(root); + + ESP_LOGI(TAG, "Received mini-program Wi-Fi credentials for SSID \"%s\"", + event.credentials.ssid); + domain_provisioning_dispatch_event(&event); +} + +static void domain_provisioning_handle_write(const uint8_t *data, + size_t data_len, + void *user_data) +{ + (void)user_data; + size_t buffered_len = 0U; + domain_provisioning_write_result_t result = + domain_provisioning_append_write_fragment(data, + data_len, + &buffered_len); + ESP_LOGI(TAG, + "Mini-program Wi-Fi write fragment: bytes=%u buffered=%u result=%d", + (unsigned)data_len, + (unsigned)buffered_len, + (int)result); + + if (result == DOMAIN_PROVISIONING_WRITE_INCOMPLETE) { + return; + } + if (result == DOMAIN_PROVISIONING_WRITE_INVALID) { + domain_wifi_provisioning_event_t event = { + .type = DOMAIN_WIFI_PROVISIONING_EVENT_BAD_JSON, + }; + domain_provisioning_dispatch_event(&event); + return; + } + domain_provisioning_process_complete_write(s_complete_write, buffered_len); +} + +static esp_err_t domain_provisioning_build_device_info( + char *advertised_name, + size_t advertised_name_len, + char *device_info, + size_t device_info_len) +{ + char device_id[DOMAIN_PROVISIONING_ID_MAX_LEN] = {0}; + char serial_number[DOMAIN_PROVISIONING_SERIAL_MAX_LEN] = {0}; + char firmware_version[DOMAIN_PROVISIONING_VERSION_MAX_LEN] = "unknown"; + domain_runtime_read_device_identity(device_id, + sizeof(device_id), + serial_number, + sizeof(serial_number)); + if (device_id[0] == '\0' || serial_number[0] == '\0') { + ESP_LOGE(TAG, "Device identity is incomplete; provisioning cannot start"); + return ESP_ERR_NOT_FOUND; + } + (void)platform_read_firmware_version(firmware_version, + sizeof(firmware_version)); + + size_t serial_len = strlen(serial_number); + const char *short_id = serial_len > 4U + ? serial_number + serial_len - 4U + : serial_number; + int written = snprintf(advertised_name, + advertised_name_len, + DOMAIN_PROVISIONING_NAME_PREFIX "%.4s", + short_id); + if (written < 0 || (size_t)written >= advertised_name_len) { + return ESP_ERR_INVALID_SIZE; + } + + cJSON *root = cJSON_CreateObject(); + if (root == NULL || + !cJSON_AddStringToObject(root, "deviceId", device_id) || + !cJSON_AddStringToObject(root, "deviceSerial", serial_number) || + !cJSON_AddStringToObject(root, "deviceName", DOMAIN_PROVISIONING_DEVICE_NAME) || + !cJSON_AddStringToObject(root, "firmwareVersion", firmware_version)) { + cJSON_Delete(root); + return ESP_ERR_NO_MEM; + } + + bool printed = cJSON_PrintPreallocated(root, + device_info, + (int)device_info_len, + false); + cJSON_Delete(root); + return printed ? ESP_OK : ESP_ERR_INVALID_SIZE; +} + +static const char *domain_provisioning_status_text( + domain_wifi_provisioning_status_t status) +{ + switch (status) { + case DOMAIN_WIFI_PROVISIONING_STATUS_WIFI_CONNECTING: + return "WIFI_CONNECTING"; + case DOMAIN_WIFI_PROVISIONING_STATUS_WIFI_GOT_IP: + return "WIFI_GOT_IP"; + case DOMAIN_WIFI_PROVISIONING_STATUS_SAVING_WIFI: + return "SAVING_WIFI"; + case DOMAIN_WIFI_PROVISIONING_STATUS_READY: + return "DEVINFO_READY"; + case DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_BAD_JSON: + return "WIFI_FAIL:BAD_JSON"; + case DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_EMPTY_SSID: + return "WIFI_FAIL:EMPTY_SSID"; + case DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_TIMEOUT: + return "WIFI_FAIL:TIMEOUT"; + case DOMAIN_WIFI_PROVISIONING_STATUS_FAIL_CONNECT: + return "WIFI_FAIL:CONNECT_FAILED"; + default: + return NULL; + } +} + +esp_err_t domain_runtime_mini_program_provisioning_start( + domain_wifi_provisioning_event_callback_t callback, + void *user_data) +{ + if (callback == NULL) { + return ESP_ERR_INVALID_ARG; + } + + char advertised_name[PLATFORM_BLE_GATT_NAME_MAX_LEN + 1] = {0}; + char device_info[DOMAIN_PROVISIONING_INFO_MAX_LEN] = {0}; + esp_err_t err = domain_provisioning_build_device_info( + advertised_name, + sizeof(advertised_name), + device_info, + sizeof(device_info)); + if (err != ESP_OK) { + return err; + } + + portENTER_CRITICAL(&s_callback_lock); + s_event_callback = callback; + s_event_user_data = user_data; + portEXIT_CRITICAL(&s_callback_lock); + domain_provisioning_reset_write_buffer(); + + platform_ble_gatt_server_config_t config = { + .advertised_name = advertised_name, + .service_uuid = s_service_uuid, + .write_uuid = s_wifi_write_uuid, + .notify_uuid = s_status_uuid, + .read_notify_uuid = s_device_info_uuid, + .read_value = (const uint8_t *)device_info, + .read_value_len = strlen(device_info), + .on_write = domain_provisioning_handle_write, + .user_data = NULL, + }; + err = platform_ble_gatt_server_start(&config); + if (err != ESP_OK) { + portENTER_CRITICAL(&s_callback_lock); + s_event_callback = NULL; + s_event_user_data = NULL; + portEXIT_CRITICAL(&s_callback_lock); + return err; + } + + ESP_LOGI(TAG, "Mini-program provisioning protocol ready"); + return ESP_OK; +} + +esp_err_t domain_runtime_mini_program_provisioning_notify( + domain_wifi_provisioning_status_t status) +{ + const char *text = domain_provisioning_status_text(status); + if (text == NULL) { + return ESP_ERR_INVALID_ARG; + } + return platform_ble_gatt_server_notify( + PLATFORM_BLE_GATT_NOTIFY_VALUE, + (const uint8_t *)text, + strlen(text)); +} + +esp_err_t domain_runtime_mini_program_provisioning_stop(int timeout_ms) +{ + esp_err_t err = platform_ble_gatt_server_stop(timeout_ms); + if (err == ESP_OK && timeout_ms > 0) { + portENTER_CRITICAL(&s_callback_lock); + s_event_callback = NULL; + s_event_user_data = NULL; + portEXIT_CRITICAL(&s_callback_lock); + domain_provisioning_reset_write_buffer(); + } + return err; +} diff --git a/components/domain/src/runtime/domain_runtime_adapters.c b/components/domain/src/runtime/domain_runtime_adapters.c index f2ea6d6..4adefec 100644 --- a/components/domain/src/runtime/domain_runtime_adapters.c +++ b/components/domain/src/runtime/domain_runtime_adapters.c @@ -76,6 +76,7 @@ static const char *TAG = "domain_rt_adapters"; static const uint8_t s_server_error_frame_order[] = {0, 1, 2, 1, 0}; static const uint8_t s_wifi_disconnected_frame_order[] = {0, 1, 2, 3, 2, 1, 0}; +static const uint8_t s_wifi_connecting_frame_order[] = {0}; static const uint8_t s_basic_face_frame_order[] = {0, 1, 2}; static const uint8_t s_muyu_frame_order[] = {0, 1, 2, 1}; static const uint8_t s_listening_frame_order[] = {0, 1, 2}; @@ -186,6 +187,21 @@ static const domain_expression_catalog_entry_t s_expression_catalog[] = { .return_to_idle = true, }, }, + { + .id = DOMAIN_EXPRESSION_WIFI_CONNECTING, + .platform_request = { + .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, + .priority = 120, + .interrupt_current = true, + .return_to_idle = false, + .asset_dir = "wifi_connecting_qr", + .frame_order = s_wifi_connecting_frame_order, + .frame_order_count = sizeof(s_wifi_connecting_frame_order) / + sizeof(s_wifi_connecting_frame_order[0]), + .repeat_count = 0, + .frame_ms = 1000, + }, + }, { .id = DOMAIN_EXPRESSION_WIFI_DISCONNECTED, .platform_request = { @@ -1237,11 +1253,24 @@ esp_err_t domain_runtime_wifi_portal_connect_sta(int timeout_ms) return platform_wifi_portal_connect_sta(timeout_ms); } +esp_err_t domain_runtime_wifi_portal_connect_sta_with_credentials( + const char *ssid, + const char *password, + int timeout_ms) +{ + return platform_wifi_portal_connect_sta_with_credentials(ssid, password, timeout_ms); +} + esp_err_t domain_runtime_wifi_portal_reconnect_sta(int timeout_ms) { return platform_wifi_portal_reconnect_sta(timeout_ms); } +esp_err_t domain_runtime_wifi_portal_stop_sta(void) +{ + return platform_wifi_portal_stop_sta(); +} + bool domain_runtime_is_network_connect_error(esp_err_t err) { return platform_https_is_connect_error(err); diff --git a/components/platform/CMakeLists.txt b/components/platform/CMakeLists.txt index cb42efe..8f31ae8 100644 --- a/components/platform/CMakeLists.txt +++ b/components/platform/CMakeLists.txt @@ -4,6 +4,7 @@ set(platform_srcs "src/runtime/platform_time.c" "src/runtime/platform_https_client.c" "src/runtime/platform_lan_http_server.c" + "src/runtime/platform_ble_gatt_server.c" "src/platform_console_driver.c" "src/runtime/platform_behavior_runtime.c" "src/runtime/platform_multimodal_runtime.c" @@ -24,6 +25,7 @@ set(platform_srcs set(platform_priv_requires 78__esp-wifi-connect + bt cxx driver esp_audio_codec @@ -135,6 +137,7 @@ set(platform_lcd_png_assets icon_huo0 icon_tu0 icon_jin0 + qrcode ) set(platform_lcd_png_asset_files) foreach(lcd_png_asset IN LISTS platform_lcd_png_assets) diff --git a/components/platform/assets/lcd/qrcode.png b/components/platform/assets/lcd/qrcode.png new file mode 100644 index 0000000..a43be6c Binary files /dev/null and b/components/platform/assets/lcd/qrcode.png differ diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index 9e6f373..fa69639 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -137,6 +137,35 @@ typedef struct { int status_code; } platform_https_response_t; +#define PLATFORM_BLE_UUID128_LEN 16 +#define PLATFORM_BLE_GATT_NAME_MAX_LEN 31 +#define PLATFORM_BLE_GATT_VALUE_MAX_LEN 512 + +typedef struct { + uint8_t bytes[PLATFORM_BLE_UUID128_LEN]; +} platform_ble_uuid128_t; + +typedef enum { + PLATFORM_BLE_GATT_NOTIFY_VALUE = 0, + PLATFORM_BLE_GATT_READ_NOTIFY_VALUE, +} platform_ble_gatt_notify_target_t; + +typedef void (*platform_ble_gatt_write_callback_t)(const uint8_t *data, + size_t data_len, + void *user_data); + +typedef struct { + const char *advertised_name; + platform_ble_uuid128_t service_uuid; + platform_ble_uuid128_t write_uuid; + platform_ble_uuid128_t notify_uuid; + platform_ble_uuid128_t read_notify_uuid; + const uint8_t *read_value; + size_t read_value_len; + platform_ble_gatt_write_callback_t on_write; + void *user_data; +} platform_ble_gatt_server_config_t; + esp_err_t platform_init(void); esp_err_t platform_prepare_storage(void); bool platform_storage_is_ready(void); @@ -225,6 +254,23 @@ esp_err_t platform_https_post_json(const platform_https_post_json_request_t *req platform_https_response_t *response); bool platform_https_is_connect_error(esp_err_t err); +/** Start a generic single-service BLE GATT peripheral. */ +esp_err_t platform_ble_gatt_server_start(const platform_ble_gatt_server_config_t *config); + +/** Send a payload to a subscribed GATT notification characteristic. */ +esp_err_t platform_ble_gatt_server_notify(platform_ble_gatt_notify_target_t target, + const uint8_t *data, + size_t data_len); + +/** + * Stop the platform-owned NimBLE runtime. A positive timeout bounds the wait; + * zero requests cooperative teardown without waiting. + */ +esp_err_t platform_ble_gatt_server_stop(int timeout_ms); + +/** Read the running application version into caller-owned storage. */ +esp_err_t platform_read_firmware_version(char *out, size_t out_len); + /** * @brief Rotate stepper synchronously with a bounded timeout. * @@ -259,7 +305,11 @@ esp_err_t platform_wifi_portal_start_config_ap(const char *ssid_prefix); * - On timeout/failure, no hidden destructive continuation is started after return. */ esp_err_t platform_wifi_portal_connect_sta(int timeout_ms); +esp_err_t platform_wifi_portal_connect_sta_with_credentials(const char *ssid, + const char *password, + int timeout_ms); esp_err_t platform_wifi_portal_reconnect_sta(int timeout_ms); +esp_err_t platform_wifi_portal_stop_sta(void); esp_err_t platform_wifi_portal_save_credentials(const char *ssid, const char *password); esp_err_t platform_wifi_portal_clear_saved_credentials(void); bool platform_wifi_connect_error_keeps_credentials(esp_err_t err); diff --git a/components/platform/internal/wifi_portal.h b/components/platform/internal/wifi_portal.h index cc525b3..6422ac0 100644 --- a/components/platform/internal/wifi_portal.h +++ b/components/platform/internal/wifi_portal.h @@ -47,7 +47,17 @@ esp_err_t wifi_portal_start_config_ap(const char *ssid_prefix); * for the connection to succeed within the provided timeout (milliseconds). */ esp_err_t wifi_portal_connect_sta(int timeout_ms); +/** + * @brief Trial-connect with credentials without persisting them first. + * + * On success, the trial credentials atomically replace the saved credential + * list. On failure, the previous in-memory and NVS credentials remain intact. + */ +esp_err_t wifi_portal_connect_sta_with_credentials(const char *ssid, + const char *password, + int timeout_ms); esp_err_t wifi_portal_reconnect_sta(int timeout_ms); +esp_err_t wifi_portal_stop_sta(void); /** * @brief Save Wi-Fi credentials into NVS. diff --git a/components/platform/src/lcd_direct_assets.c b/components/platform/src/lcd_direct_assets.c index 812ca64..0279b7a 100644 --- a/components/platform/src/lcd_direct_assets.c +++ b/components/platform/src/lcd_direct_assets.c @@ -26,6 +26,10 @@ static const lcd_direct_image_t s_listening_frames[] = { {.path = LCD_ASSET_BASE_PATH "listening2.png"}, }; +static const lcd_direct_image_t s_wifi_connecting_qr_frames[] = { + {.path = LCD_ASSET_BASE_PATH "qrcode.png"}, +}; + static const lcd_direct_image_t s_muyu_frames[] = { { .path = LCD_ASSET_BASE_PATH "muyu0.png", @@ -248,6 +252,14 @@ const lcd_direct_image_t *lcd_direct_asset_frame(const char *asset_dir, uint8_t frame_index); } + if (strcmp(asset_dir, "wifi_connecting_qr") == 0) { + return lcd_direct_frame_from_array( + s_wifi_connecting_qr_frames, + sizeof(s_wifi_connecting_qr_frames) / + sizeof(s_wifi_connecting_qr_frames[0]), + frame_index); + } + if (strcmp(asset_dir, "muyu") == 0) { return lcd_direct_frame_from_array(s_muyu_frames, sizeof(s_muyu_frames) / sizeof(s_muyu_frames[0]), diff --git a/components/platform/src/runtime/platform_behavior_runtime.c b/components/platform/src/runtime/platform_behavior_runtime.c index a07cc54..048fe2b 100644 --- a/components/platform/src/runtime/platform_behavior_runtime.c +++ b/components/platform/src/runtime/platform_behavior_runtime.c @@ -138,11 +138,23 @@ esp_err_t platform_wifi_portal_connect_sta(int timeout_ms) return wifi_portal_connect_sta(timeout_ms); } +esp_err_t platform_wifi_portal_connect_sta_with_credentials(const char *ssid, + const char *password, + int timeout_ms) +{ + return wifi_portal_connect_sta_with_credentials(ssid, password, timeout_ms); +} + esp_err_t platform_wifi_portal_reconnect_sta(int timeout_ms) { return wifi_portal_reconnect_sta(timeout_ms); } +esp_err_t platform_wifi_portal_stop_sta(void) +{ + return wifi_portal_stop_sta(); +} + esp_err_t platform_wifi_portal_save_credentials(const char *ssid, const char *password) { return wifi_portal_save_credentials(ssid, password); diff --git a/components/platform/src/runtime/platform_ble_gatt_server.c b/components/platform/src/runtime/platform_ble_gatt_server.c new file mode 100644 index 0000000..b9f3454 --- /dev/null +++ b/components/platform/src/runtime/platform_ble_gatt_server.c @@ -0,0 +1,616 @@ +/* + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "platform.h" + +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/task.h" +#include "sdkconfig.h" + +static const char *TAG = "platform_ble_gatt"; + +#if CONFIG_BT_NIMBLE_ENABLED + +#include "host/ble_att.h" +#include "host/ble_hs.h" +#include "host/ble_hs_mbuf.h" +#include "host/ble_uuid.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "services/gap/ble_svc_gap.h" +#include "services/gatt/ble_svc_gatt.h" + +#define PLATFORM_BLE_SYNC_TIMEOUT_MS 3000 +#define PLATFORM_BLE_PREFERRED_MTU 256 +#define PLATFORM_BLE_STOP_DONE_BIT BIT0 +#define PLATFORM_BLE_STOP_TASK_STACK_WORDS 4096 +#define PLATFORM_BLE_STOP_TASK_PRIORITY 5 +#define PLATFORM_BLE_STOP_MAX_ATTEMPTS 3 +#define PLATFORM_BLE_STOP_RETRY_DELAY_MS 100 + +typedef struct { + bool started; + bool synced; + bool stopping; + bool stop_failed; + bool host_stopped; + uint8_t own_addr_type; + uint16_t connection_handle; + bool notify_subscribed; + bool read_notify_subscribed; + char advertised_name[PLATFORM_BLE_GATT_NAME_MAX_LEN + 1]; + uint8_t read_value[PLATFORM_BLE_GATT_VALUE_MAX_LEN]; + size_t read_value_len; + platform_ble_gatt_write_callback_t on_write; + void *user_data; +} platform_ble_gatt_state_t; + +static portMUX_TYPE s_state_lock = portMUX_INITIALIZER_UNLOCKED; +static platform_ble_gatt_state_t s_state = { + .connection_handle = BLE_HS_CONN_HANDLE_NONE, +}; +static ble_uuid128_t s_service_uuid; +static ble_uuid128_t s_write_uuid; +static ble_uuid128_t s_notify_uuid; +static ble_uuid128_t s_read_notify_uuid; +static uint16_t s_write_value_handle; +static uint16_t s_notify_value_handle; +static uint16_t s_read_notify_value_handle; +static SemaphoreHandle_t s_sync_sem; +static EventGroupHandle_t s_stop_events; +static TaskHandle_t s_stop_task; +static esp_err_t s_sync_result = ESP_ERR_INVALID_STATE; +static esp_err_t s_stop_result = ESP_OK; + +static int platform_ble_gatt_access(uint16_t connection_handle, + uint16_t attribute_handle, + struct ble_gatt_access_ctxt *context, + void *arg); +static int platform_ble_gap_event(struct ble_gap_event *event, void *arg); + +static const struct ble_gatt_svc_def s_gatt_services[] = { + { + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &s_service_uuid.u, + .characteristics = (struct ble_gatt_chr_def[]) { + { + .uuid = &s_write_uuid.u, + .access_cb = platform_ble_gatt_access, + .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP, + .val_handle = &s_write_value_handle, + }, + { + .uuid = &s_notify_uuid.u, + .access_cb = platform_ble_gatt_access, + .flags = BLE_GATT_CHR_F_NOTIFY, + .val_handle = &s_notify_value_handle, + }, + { + .uuid = &s_read_notify_uuid.u, + .access_cb = platform_ble_gatt_access, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, + .val_handle = &s_read_notify_value_handle, + }, + {0}, + }, + }, + {0}, +}; + +static void platform_ble_set_uuid(ble_uuid128_t *out, + const platform_ble_uuid128_t *source) +{ + memset(out, 0, sizeof(*out)); + out->u.type = BLE_UUID_TYPE_128; + memcpy(out->value, source->bytes, sizeof(out->value)); +} + +static esp_err_t platform_ble_validate_config( + const platform_ble_gatt_server_config_t *config) +{ + if (config == NULL || config->advertised_name == NULL || + config->advertised_name[0] == '\0' || config->on_write == NULL || + config->read_value == NULL || config->read_value_len == 0U) { + return ESP_ERR_INVALID_ARG; + } + if (strlen(config->advertised_name) > PLATFORM_BLE_GATT_NAME_MAX_LEN || + config->read_value_len > PLATFORM_BLE_GATT_VALUE_MAX_LEN) { + return ESP_ERR_INVALID_SIZE; + } + return ESP_OK; +} + +static void platform_ble_clear_runtime_state(void) +{ + portENTER_CRITICAL(&s_state_lock); + s_state.started = false; + s_state.synced = false; + s_state.stopping = false; + s_state.stop_failed = false; + s_state.host_stopped = false; + s_state.connection_handle = BLE_HS_CONN_HANDLE_NONE; + s_state.notify_subscribed = false; + s_state.read_notify_subscribed = false; + s_state.on_write = NULL; + s_state.user_data = NULL; + portEXIT_CRITICAL(&s_state_lock); +} + +static esp_err_t platform_ble_start_advertising(void) +{ + struct ble_hs_adv_fields fields = {0}; + struct ble_gap_adv_params parameters = {0}; + char advertised_name[PLATFORM_BLE_GATT_NAME_MAX_LEN + 1]; + uint8_t own_addr_type; + + portENTER_CRITICAL(&s_state_lock); + strlcpy(advertised_name, s_state.advertised_name, sizeof(advertised_name)); + own_addr_type = s_state.own_addr_type; + portEXIT_CRITICAL(&s_state_lock); + + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + fields.name = (uint8_t *)advertised_name; + fields.name_len = strlen(advertised_name); + fields.name_is_complete = 1; + + int rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + ESP_LOGE(TAG, "Set advertising fields failed: rc=%d", rc); + return ESP_FAIL; + } + + parameters.conn_mode = BLE_GAP_CONN_MODE_UND; + parameters.disc_mode = BLE_GAP_DISC_MODE_GEN; + rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, + ¶meters, platform_ble_gap_event, NULL); + if (rc != 0 && rc != BLE_HS_EALREADY) { + ESP_LOGE(TAG, "Start advertising failed: rc=%d", rc); + return ESP_FAIL; + } + ESP_LOGI(TAG, "BLE provisioning advertising as %s", advertised_name); + return ESP_OK; +} + +static void platform_ble_on_reset(int reason) +{ + ESP_LOGW(TAG, "NimBLE host reset: reason=%d", reason); +} + +static void platform_ble_on_sync(void) +{ + uint8_t own_addr_type = 0; + int rc = ble_hs_id_infer_auto(0, &own_addr_type); + esp_err_t result = ESP_OK; + if (rc != 0) { + ESP_LOGE(TAG, "Infer BLE identity address failed: rc=%d", rc); + result = ESP_FAIL; + } else { + portENTER_CRITICAL(&s_state_lock); + s_state.own_addr_type = own_addr_type; + portEXIT_CRITICAL(&s_state_lock); + result = platform_ble_start_advertising(); + } + + portENTER_CRITICAL(&s_state_lock); + s_state.synced = (result == ESP_OK); + s_sync_result = result; + portEXIT_CRITICAL(&s_state_lock); + if (s_sync_sem != NULL) { + (void)xSemaphoreGive(s_sync_sem); + } +} + +static void platform_ble_host_task(void *arg) +{ + (void)arg; + nimble_port_run(); + nimble_port_freertos_deinit(); +} + +static int platform_ble_gatt_access(uint16_t connection_handle, + uint16_t attribute_handle, + struct ble_gatt_access_ctxt *context, + void *arg) +{ + (void)connection_handle; + (void)arg; + + if (context->op == BLE_GATT_ACCESS_OP_READ_CHR && + attribute_handle == s_read_notify_value_handle) { + int rc = os_mbuf_append(context->om, s_state.read_value, s_state.read_value_len); + return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; + } + + if (context->op == BLE_GATT_ACCESS_OP_WRITE_CHR && + attribute_handle == s_write_value_handle) { + uint16_t payload_len = OS_MBUF_PKTLEN(context->om); + if (payload_len == 0U || payload_len > PLATFORM_BLE_GATT_VALUE_MAX_LEN) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + + uint8_t payload[PLATFORM_BLE_GATT_VALUE_MAX_LEN]; + uint16_t copied_len = 0; + int rc = ble_hs_mbuf_to_flat(context->om, payload, sizeof(payload), &copied_len); + if (rc != 0 || copied_len != payload_len) { + return BLE_ATT_ERR_UNLIKELY; + } + + platform_ble_gatt_write_callback_t callback; + void *user_data; + portENTER_CRITICAL(&s_state_lock); + callback = s_state.on_write; + user_data = s_state.user_data; + portEXIT_CRITICAL(&s_state_lock); + if (callback != NULL) { + ESP_LOGI(TAG, + "BLE provisioning write fragment received: bytes=%u", + (unsigned)copied_len); + callback(payload, copied_len, user_data); + ESP_LOGD(TAG, + "BLE provisioning write callback stack hwm=%uB", + (unsigned)uxTaskGetStackHighWaterMark(NULL)); + } + return 0; + } + + return BLE_ATT_ERR_UNLIKELY; +} + +static int platform_ble_gap_event(struct ble_gap_event *event, void *arg) +{ + (void)arg; + + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + portENTER_CRITICAL(&s_state_lock); + s_state.connection_handle = event->connect.conn_handle; + s_state.notify_subscribed = false; + s_state.read_notify_subscribed = false; + portEXIT_CRITICAL(&s_state_lock); + ESP_LOGI(TAG, "BLE provisioning client connected"); + } else { + (void)platform_ble_start_advertising(); + } + return 0; + + case BLE_GAP_EVENT_DISCONNECT: { + portENTER_CRITICAL(&s_state_lock); + s_state.connection_handle = BLE_HS_CONN_HANDLE_NONE; + s_state.notify_subscribed = false; + s_state.read_notify_subscribed = false; + bool restart_advertising = s_state.started && !s_state.stopping; + portEXIT_CRITICAL(&s_state_lock); + ESP_LOGI(TAG, "BLE provisioning client disconnected: reason=%d", + event->disconnect.reason); + if (restart_advertising) { + (void)platform_ble_start_advertising(); + } + return 0; + } + + case BLE_GAP_EVENT_SUBSCRIBE: { + bool send_device_info = false; + portENTER_CRITICAL(&s_state_lock); + if (event->subscribe.attr_handle == s_notify_value_handle) { + s_state.notify_subscribed = event->subscribe.cur_notify != 0; + } else if (event->subscribe.attr_handle == s_read_notify_value_handle) { + s_state.read_notify_subscribed = event->subscribe.cur_notify != 0; + send_device_info = s_state.read_notify_subscribed; + } + portEXIT_CRITICAL(&s_state_lock); + if (send_device_info) { + (void)platform_ble_gatt_server_notify( + PLATFORM_BLE_GATT_READ_NOTIFY_VALUE, + s_state.read_value, + s_state.read_value_len); + } + return 0; + } + + case BLE_GAP_EVENT_MTU: + ESP_LOGD(TAG, "BLE ATT MTU updated: mtu=%u", event->mtu.value); + return 0; + + default: + return 0; + } +} + +esp_err_t platform_ble_gatt_server_notify(platform_ble_gatt_notify_target_t target, + const uint8_t *data, + size_t data_len) +{ + if (data == NULL || data_len == 0U || + (target != PLATFORM_BLE_GATT_NOTIFY_VALUE && + target != PLATFORM_BLE_GATT_READ_NOTIFY_VALUE)) { + return ESP_ERR_INVALID_ARG; + } + + uint16_t connection_handle; + uint16_t value_handle; + bool subscribed; + portENTER_CRITICAL(&s_state_lock); + connection_handle = s_state.connection_handle; + if (target == PLATFORM_BLE_GATT_NOTIFY_VALUE) { + value_handle = s_notify_value_handle; + subscribed = s_state.notify_subscribed; + } else { + value_handle = s_read_notify_value_handle; + subscribed = s_state.read_notify_subscribed; + } + portEXIT_CRITICAL(&s_state_lock); + + if (connection_handle == BLE_HS_CONN_HANDLE_NONE || !subscribed) { + return ESP_ERR_INVALID_STATE; + } + + uint16_t mtu = ble_att_mtu(connection_handle); + size_t fragment_max = mtu > 3U ? (size_t)mtu - 3U : 20U; + for (size_t offset = 0; offset < data_len; offset += fragment_max) { + size_t fragment_len = data_len - offset; + if (fragment_len > fragment_max) { + fragment_len = fragment_max; + } + struct os_mbuf *packet = ble_hs_mbuf_from_flat(data + offset, fragment_len); + if (packet == NULL) { + return ESP_ERR_NO_MEM; + } + int rc = ble_gatts_notify_custom(connection_handle, value_handle, packet); + if (rc != 0) { + ESP_LOGW(TAG, "BLE notification failed: rc=%d", rc); + return ESP_FAIL; + } + } + return ESP_OK; +} + +static void platform_ble_stop_task(void *arg) +{ + (void)arg; + bool host_stopped = false; + portENTER_CRITICAL(&s_state_lock); + host_stopped = s_state.host_stopped; + portEXIT_CRITICAL(&s_state_lock); + + esp_err_t result = ESP_FAIL; + if (!host_stopped) { + for (int attempt = 1; attempt <= PLATFORM_BLE_STOP_MAX_ATTEMPTS; ++attempt) { + int rc = nimble_port_stop(); + if (rc == 0) { + host_stopped = true; + break; + } + ESP_LOGW(TAG, + "Cooperative NimBLE stop failed: attempt=%d/%d rc=%d", + attempt, + PLATFORM_BLE_STOP_MAX_ATTEMPTS, + rc); + if (attempt < PLATFORM_BLE_STOP_MAX_ATTEMPTS) { + vTaskDelay(pdMS_TO_TICKS(PLATFORM_BLE_STOP_RETRY_DELAY_MS)); + } + } + } + + if (host_stopped) { + for (int attempt = 1; attempt <= PLATFORM_BLE_STOP_MAX_ATTEMPTS; ++attempt) { + result = nimble_port_deinit(); + if (result == ESP_OK) { + break; + } + ESP_LOGW(TAG, + "NimBLE deinit failed: attempt=%d/%d err=%s", + attempt, + PLATFORM_BLE_STOP_MAX_ATTEMPTS, + esp_err_to_name(result)); + if (attempt < PLATFORM_BLE_STOP_MAX_ATTEMPTS) { + vTaskDelay(pdMS_TO_TICKS(PLATFORM_BLE_STOP_RETRY_DELAY_MS)); + } + } + } + + portENTER_CRITICAL(&s_state_lock); + s_stop_result = result; + if (result == ESP_OK) { + s_state.started = false; + s_state.synced = false; + s_state.stopping = false; + s_state.stop_failed = false; + s_state.host_stopped = false; + s_state.connection_handle = BLE_HS_CONN_HANDLE_NONE; + s_state.notify_subscribed = false; + s_state.read_notify_subscribed = false; + s_state.on_write = NULL; + s_state.user_data = NULL; + } else { + s_state.stopping = true; + s_state.stop_failed = true; + s_state.host_stopped = host_stopped; + } + s_stop_task = NULL; + portEXIT_CRITICAL(&s_state_lock); + if (s_stop_events != NULL) { + (void)xEventGroupSetBits(s_stop_events, PLATFORM_BLE_STOP_DONE_BIT); + } + vTaskDelete(NULL); +} + +esp_err_t platform_ble_gatt_server_start(const platform_ble_gatt_server_config_t *config) +{ + esp_err_t err = platform_ble_validate_config(config); + if (err != ESP_OK) { + return err; + } + + portENTER_CRITICAL(&s_state_lock); + if (s_state.stopping || s_state.stop_failed) { + portEXIT_CRITICAL(&s_state_lock); + return ESP_ERR_INVALID_STATE; + } + if (s_state.started) { + portEXIT_CRITICAL(&s_state_lock); + return ESP_OK; + } + strlcpy(s_state.advertised_name, config->advertised_name, + sizeof(s_state.advertised_name)); + memcpy(s_state.read_value, config->read_value, config->read_value_len); + s_state.read_value_len = config->read_value_len; + s_state.on_write = config->on_write; + s_state.user_data = config->user_data; + s_state.connection_handle = BLE_HS_CONN_HANDLE_NONE; + s_state.started = true; + s_state.synced = false; + s_state.stop_failed = false; + s_state.host_stopped = false; + portEXIT_CRITICAL(&s_state_lock); + + platform_ble_set_uuid(&s_service_uuid, &config->service_uuid); + platform_ble_set_uuid(&s_write_uuid, &config->write_uuid); + platform_ble_set_uuid(&s_notify_uuid, &config->notify_uuid); + platform_ble_set_uuid(&s_read_notify_uuid, &config->read_notify_uuid); + + if (s_sync_sem == NULL) { + s_sync_sem = xSemaphoreCreateBinary(); + } + if (s_stop_events == NULL) { + s_stop_events = xEventGroupCreate(); + } + if (s_sync_sem == NULL || s_stop_events == NULL) { + platform_ble_clear_runtime_state(); + return ESP_ERR_NO_MEM; + } + while (xSemaphoreTake(s_sync_sem, 0) == pdTRUE) { + } + (void)xEventGroupClearBits(s_stop_events, PLATFORM_BLE_STOP_DONE_BIT); + + err = nimble_port_init(); + if (err != ESP_OK) { + platform_ble_clear_runtime_state(); + return err; + } + + ble_hs_cfg.reset_cb = platform_ble_on_reset; + ble_hs_cfg.sync_cb = platform_ble_on_sync; + ble_svc_gap_init(); + ble_svc_gatt_init(); + + int rc = ble_gatts_count_cfg(s_gatt_services); + if (rc == 0) { + rc = ble_gatts_add_svcs(s_gatt_services); + } + if (rc == 0) { + rc = ble_svc_gap_device_name_set(s_state.advertised_name); + } + if (rc == 0) { + rc = ble_att_set_preferred_mtu(PLATFORM_BLE_PREFERRED_MTU); + } + if (rc != 0) { + ESP_LOGE(TAG, "Configure NimBLE GATT server failed: rc=%d", rc); + (void)nimble_port_deinit(); + platform_ble_clear_runtime_state(); + return ESP_FAIL; + } + + s_sync_result = ESP_ERR_INVALID_STATE; + nimble_port_freertos_init(platform_ble_host_task); + if (xSemaphoreTake(s_sync_sem, + pdMS_TO_TICKS(PLATFORM_BLE_SYNC_TIMEOUT_MS)) != pdTRUE) { + ESP_LOGE(TAG, "NimBLE host sync timed out"); + (void)platform_ble_gatt_server_stop(PLATFORM_BLE_SYNC_TIMEOUT_MS); + return ESP_ERR_TIMEOUT; + } + if (s_sync_result != ESP_OK) { + (void)platform_ble_gatt_server_stop(PLATFORM_BLE_SYNC_TIMEOUT_MS); + } + return s_sync_result; +} + +esp_err_t platform_ble_gatt_server_stop(int timeout_ms) +{ + if (timeout_ms < 0) { + return ESP_ERR_INVALID_ARG; + } + + bool create_stop_task = false; + portENTER_CRITICAL(&s_state_lock); + if (!s_state.started && !s_state.stopping) { + portEXIT_CRITICAL(&s_state_lock); + return ESP_OK; + } + if (!s_state.stopping) { + s_state.stopping = true; + create_stop_task = true; + } else if (s_stop_task == NULL && s_state.stop_failed) { + s_state.stop_failed = false; + create_stop_task = true; + } + portEXIT_CRITICAL(&s_state_lock); + + if (create_stop_task) { + (void)xEventGroupClearBits(s_stop_events, PLATFORM_BLE_STOP_DONE_BIT); + BaseType_t created = xTaskCreate(platform_ble_stop_task, "ble_gatt_stop", + PLATFORM_BLE_STOP_TASK_STACK_WORDS, NULL, + PLATFORM_BLE_STOP_TASK_PRIORITY, &s_stop_task); + if (created != pdPASS) { + portENTER_CRITICAL(&s_state_lock); + s_state.stopping = true; + s_state.stop_failed = true; + s_stop_task = NULL; + portEXIT_CRITICAL(&s_state_lock); + return ESP_ERR_NO_MEM; + } + } + + if (timeout_ms == 0) { + return ESP_OK; + } + + EventBits_t bits = xEventGroupWaitBits(s_stop_events, PLATFORM_BLE_STOP_DONE_BIT, + pdFALSE, pdTRUE, + pdMS_TO_TICKS(timeout_ms)); + if ((bits & PLATFORM_BLE_STOP_DONE_BIT) != 0) { + esp_err_t result = ESP_FAIL; + portENTER_CRITICAL(&s_state_lock); + result = s_stop_result; + portEXIT_CRITICAL(&s_state_lock); + return result; + } + + ESP_LOGW(TAG, + "NimBLE stop timed out after %d ms; cooperative teardown remains active", + timeout_ms); + return ESP_ERR_TIMEOUT; +} + +#else + +esp_err_t platform_ble_gatt_server_start(const platform_ble_gatt_server_config_t *config) +{ + (void)config; + ESP_LOGW(TAG, "NimBLE is disabled in this firmware configuration"); + return ESP_ERR_NOT_SUPPORTED; +} + +esp_err_t platform_ble_gatt_server_notify(platform_ble_gatt_notify_target_t target, + const uint8_t *data, + size_t data_len) +{ + (void)target; + (void)data; + (void)data_len; + return ESP_ERR_NOT_SUPPORTED; +} + +esp_err_t platform_ble_gatt_server_stop(int timeout_ms) +{ + return timeout_ms < 0 ? ESP_ERR_INVALID_ARG : ESP_OK; +} + +#endif diff --git a/components/platform/src/runtime/platform_core_runtime.c b/components/platform/src/runtime/platform_core_runtime.c index 193ecd3..0c877f1 100644 --- a/components/platform/src/runtime/platform_core_runtime.c +++ b/components/platform/src/runtime/platform_core_runtime.c @@ -6,6 +6,9 @@ #include "platform.h" +#include + +#include "esp_app_desc.h" #include "esp_log.h" #include "nvs.h" #include "nvs_flash.h" @@ -29,6 +32,25 @@ static const char *const s_known_nvs_namespaces[] = { "fortune", }; +esp_err_t platform_read_firmware_version(char *out, size_t out_len) +{ + if (out == NULL || out_len == 0U) { + return ESP_ERR_INVALID_ARG; + } + + const esp_app_desc_t *description = esp_app_get_description(); + if (description == NULL || description->version[0] == '\0') { + out[0] = '\0'; + return ESP_ERR_NOT_FOUND; + } + if (strnlen(description->version, sizeof(description->version)) >= out_len) { + out[0] = '\0'; + return ESP_ERR_INVALID_SIZE; + } + strlcpy(out, description->version, out_len); + return ESP_OK; +} + static void platform_log_nvs_stats(void) { nvs_stats_t stats = {0}; diff --git a/components/platform/src/wifi_portal_bridge.cpp b/components/platform/src/wifi_portal_bridge.cpp index 108b328..5c05250 100644 --- a/components/platform/src/wifi_portal_bridge.cpp +++ b/components/platform/src/wifi_portal_bridge.cpp @@ -6,6 +6,7 @@ #include "esp_check.h" #include "esp_err.h" #include "esp_log.h" +#include "esp_netif.h" #include "esp_timer.h" #include "esp_wifi.h" #include "freertos/FreeRTOS.h" @@ -42,6 +43,7 @@ static StaticSemaphore_t s_portal_mutex_buf; #endif static bool s_wifi_manager_initialized; +static bool s_credential_trial_active; static std::string s_config_ap_ssid_prefix = "TQ-CAT"; static wifi_portal_behavior_hooks_t s_behavior_hooks = {}; @@ -343,6 +345,143 @@ esp_err_t wifi_portal_connect_sta(int timeout_ms) return ESP_ERR_TIMEOUT; } +esp_err_t wifi_portal_connect_sta_with_credentials(const char *ssid, + const char *password, + int timeout_ms) +{ + ESP_RETURN_ON_FALSE(ssid != NULL && ssid[0] != '\0', + ESP_ERR_INVALID_ARG, + TAG, + "SSID missing"); + ESP_RETURN_ON_FALSE(strlen(ssid) <= 32, ESP_ERR_INVALID_ARG, TAG, "SSID too long"); + ESP_RETURN_ON_FALSE(password == NULL || strlen(password) <= 64, + ESP_ERR_INVALID_ARG, + TAG, + "Password too long"); + + int effective_timeout_ms = normalize_connect_timeout_ms(timeout_ms); + int trial_timeout_ms = effective_timeout_ms * 2 / 3; + if (trial_timeout_ms <= 0) { + trial_timeout_ms = effective_timeout_ms; + } + int64_t deadline_us = esp_timer_get_time() + (int64_t)effective_timeout_ms * 1000; + + ESP_RETURN_ON_ERROR(wifi_portal_stop_sta(), + TAG, + "Failed to stop STA before credential trial"); + esp_netif_t *trial_netif = NULL; + esp_err_t setup_err = ESP_OK; + { + ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); + PortalUnlockGuard guard; + ESP_RETURN_ON_FALSE(!s_credential_trial_active, + ESP_ERR_INVALID_STATE, + TAG, + "Wi-Fi credential trial already active"); + s_credential_trial_active = true; + setup_err = ensure_wifi_manager_initialized(); + if (setup_err == ESP_OK) { + trial_netif = esp_netif_create_default_wifi_sta(); + if (trial_netif == NULL) { + setup_err = ESP_ERR_NO_MEM; + } + } + if (setup_err == ESP_OK) { + setup_err = esp_wifi_set_mode(WIFI_MODE_STA); + } + if (setup_err == ESP_OK) { + setup_err = esp_wifi_start(); + } + if (setup_err == ESP_OK) { + wifi_config_t config = {}; + std::strncpy(reinterpret_cast(config.sta.ssid), + ssid, + sizeof(config.sta.ssid) - 1U); + std::strncpy(reinterpret_cast(config.sta.password), + password != NULL ? password : "", + sizeof(config.sta.password) - 1U); + config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; + config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL; + config.sta.threshold.authmode = WIFI_AUTH_OPEN; + config.sta.pmf_cfg.capable = true; + config.sta.pmf_cfg.required = false; + setup_err = esp_wifi_set_config(WIFI_IF_STA, &config); + } + if (setup_err == ESP_OK) { + setup_err = apply_sta_stability_policy(); + } + if (setup_err == ESP_OK) { + setup_err = esp_wifi_connect(); + } + ESP_LOGI(TAG, "Starting non-persistent credential trial for SSID \"%s\"", ssid); + } + + bool got_ip = false; + if (setup_err == ESP_OK) { + int64_t trial_deadline_us = esp_timer_get_time() + (int64_t)trial_timeout_ms * 1000; + if (trial_deadline_us > deadline_us) { + trial_deadline_us = deadline_us; + } + while (esp_timer_get_time() < trial_deadline_us) { + esp_netif_ip_info_t ip_info = {}; + wifi_ap_record_t ap_info = {}; + if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK && + esp_netif_get_ip_info(trial_netif, &ip_info) == ESP_OK && + ip_info.ip.addr != 0U) { + got_ip = true; + break; + } + vTaskDelay(pdMS_TO_TICKS(WIFI_PORTAL_CONNECT_POLL_MS)); + } + } + + (void)esp_wifi_disconnect(); + (void)esp_wifi_stop(); + if (trial_netif != NULL) { + esp_netif_destroy_default_wifi(trial_netif); + } + + ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); + s_credential_trial_active = false; + (void)xSemaphoreGiveRecursive(s_portal_mutex); + + if (!got_ip) { + ESP_LOGW(TAG, + "Credential trial failed for SSID \"%s\"; previous credentials preserved", + ssid); + return setup_err == ESP_OK ? ESP_ERR_TIMEOUT : setup_err; + } + + { + ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); + PortalUnlockGuard guard; + auto &credentials = SsidManager::GetInstance(); + credentials.Clear(); + credentials.AddSsid(ssid, password != NULL ? password : ""); + ESP_LOGI(TAG, + "Credential trial succeeded and replaced saved Wi-Fi for SSID \"%s\"", + ssid); + } + + int remaining_ms = (int)((deadline_us - esp_timer_get_time()) / 1000); + if (remaining_ms <= 0) { + remaining_ms = 1; + } + return wifi_portal_connect_sta(remaining_ms); +} + +esp_err_t wifi_portal_stop_sta(void) +{ + ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal"); + PortalUnlockGuard guard; + + if (!s_wifi_manager_initialized) { + return ESP_OK; + } + WifiManager::GetInstance().StopStation(); + return ESP_OK; +} + esp_err_t wifi_portal_reconnect_sta(int timeout_ms) { int effective_timeout_ms = normalize_connect_timeout_ms(timeout_ms); diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 9d61a4c..bab1aea 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -45,6 +45,30 @@ menu "Example Configuration" endmenu + menu "Mini Program Wi-Fi Provisioning" + + config TQ_MINI_PROGRAM_PROVISIONING_ENABLE + bool "Enable mini-program BLE-assisted Wi-Fi provisioning" + depends on BT_NIMBLE_ENABLED + default y + help + Advertise the fixed TalkingQ GATT service when no usable Wi-Fi + credentials exist. No SoftAP fallback is started. + + config TQ_MINI_PROGRAM_PROVISIONING_CONNECT_TIMEOUT_MS + int "Provisioning Wi-Fi connection timeout (ms)" + depends on TQ_MINI_PROGRAM_PROVISIONING_ENABLE + default 30000 + range 5000 120000 + + config TQ_MINI_PROGRAM_PROVISIONING_RECONNECT_FAILURES + int "Runtime reconnect failures before BLE reprovisioning" + depends on TQ_MINI_PROGRAM_PROVISIONING_ENABLE + default 2 + range 1 10 + + endmenu + menu "Runtime Diagnostics" config TQ_BOOT_SEQUENCE_DRY_RUN diff --git a/sdkconfig.defaults b/sdkconfig.defaults index dd7d715..187c280 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -2,6 +2,20 @@ CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y CONFIG_ESPTOOLPY_FLASHSIZE="16MB" CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y +# Mini-program BLE provisioning. +CONFIG_BT_ENABLED=y +# CONFIG_BT_BLUEDROID_ENABLED is not set +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y +CONFIG_BT_NIMBLE_ROLE_CENTRAL=n +CONFIG_BT_NIMBLE_ROLE_OBSERVER=n +CONFIG_BT_NIMBLE_ROLE_BROADCASTER=y +CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 +CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=6144 +CONFIG_TQ_MINI_PROGRAM_PROVISIONING_ENABLE=y +CONFIG_TQ_MINI_PROGRAM_PROVISIONING_CONNECT_TIMEOUT_MS=30000 +CONFIG_TQ_MINI_PROGRAM_PROVISIONING_RECONNECT_FAILURES=2 + # External PSRAM (ESP32-S3 octal mode) CONFIG_SPIRAM=y CONFIG_SPIRAM_MODE_OCT=y @@ -129,8 +143,8 @@ CONFIG_MDNS_MEMORY_ALLOC_SPIRAM=y CONFIG_MULTIMODAL_DEVICE_ID="" CONFIG_MULTIMODAL_SERIAL_NUMBER="" -CONFIG_MULTIMODAL_WIFI_SSID="TalkingQ_Guest" -CONFIG_MULTIMODAL_WIFI_PASSWORD="66668888" +CONFIG_MULTIMODAL_WIFI_SSID="" +CONFIG_MULTIMODAL_WIFI_PASSWORD="" CONFIG_ESPTOOLPY_MONITOR_BAUD=921600 CONFIG_ESP_CONSOLE_UART_BAUDRATE=921600 CONFIG_MULTIMODAL_DEVICE_TOKEN_URL="https://gz.api.talkingq.com/token/api/device/token"