From 841fe32c4927b97a7fbe1908b765f93ae17a8225 Mon Sep 17 00:00:00 2001 From: chenjintao Date: Thu, 13 Aug 2026 15:36:42 +0800 Subject: [PATCH] feat(provisioning): add mini-program BLE Wi-Fi setup --- AGENTS.md | 1 + .../runtime/app_composition_default_runtime.c | 25 +- .../control_plane/include/control_plane.h | 7 +- .../internal/control_plane_internal.h | 1 + .../control_plane_connectivity_runtime.c | 313 ++++++++- .../runtime/control_plane_runtime_workers.c | 69 +- components/domain/CMakeLists.txt | 1 + components/domain/include/domain.h | 55 ++ .../domain_mini_program_provisioning.c | 410 ++++++++++++ .../src/runtime/domain_runtime_adapters.c | 29 + components/platform/CMakeLists.txt | 3 + components/platform/assets/lcd/qrcode.png | Bin 0 -> 18971 bytes components/platform/include/platform.h | 50 ++ components/platform/internal/wifi_portal.h | 10 + components/platform/src/lcd_direct_assets.c | 12 + .../src/runtime/platform_behavior_runtime.c | 12 + .../src/runtime/platform_ble_gatt_server.c | 616 ++++++++++++++++++ .../src/runtime/platform_core_runtime.c | 22 + .../platform/src/wifi_portal_bridge.cpp | 139 ++++ main/Kconfig.projbuild | 24 + sdkconfig.defaults | 18 +- 21 files changed, 1776 insertions(+), 41 deletions(-) create mode 100644 components/domain/src/runtime/domain_mini_program_provisioning.c create mode 100644 components/platform/assets/lcd/qrcode.png create mode 100644 components/platform/src/runtime/platform_ble_gatt_server.c 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 0000000000000000000000000000000000000000..a43be6cd2ce2e06475b96851ba0b625476878c01 GIT binary patch literal 18971 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

!sq>ceLtVi`vO>r`qhM_=PrFsd;G0>4>Ni3 z7;zBuPhABZWBzc1IzWsV9TE`BpD7~aOZtc{yQR}|hnhH#fYmf1RU@uUacc9>HQ6i$ zeK8_VqQ{q^#^8&0P+l(a8k?o^^)~Jvvb=szw*~9~n=?>LO6m(}nQ(0!TGol`ex>qx z#u0(p$jlgERuX=X;ZK1R<}^0_vtMKBcJX&J9>Yq`#DDR6o-?T{;jJEY;XIa(QvxjW z|AE-}-)jD%g>9vc#w5biD!JgKCf73uR@~dwB}$d2G*R{{Jk@tQj?uqaYfqVaG#mBb zxoiaef&W-X??tV;!)W*_f>eON5^B5j9>`FZy6PPVdYc!2Jku}HO)B`73ob!$)WWkt zAY~e4CSQA`LQ8s(y}5FDzuZa^y`sn;W<*Q;d=IxJg4!yD6jlFR)9?p$R=6MYbDhP*oRWT8U4SN-6+>fD%~?=K*veixZx zObEDTl5%g~c9>|2sVVY)p5@my%gKp)@QtIM3bpKiaVSTNmIT1v<&Emx3pU@SgN54d zz+@bUxz7-e#psU0aNO*A&BklXGUa4K(tZ7|-eU@#EFGD-zIJI}jI36CaO&+@tDnwb zlmSaT%u{$PY9Y%r2f{+Jm`fIQ+mMPscsyb;9eUy?5^m`9~k92J$u+jZ=aA&nuffGIl;?;%}t7(vV%?tvcD5~fby`hO2 zhNCefpuU|SXJ;!M{+Xhs9m9|c(q>!*yL{J=h2 zrK6fAfZ~=6xCvw1FH>Z9!70MVXSbcIoVMJ<$IipY0wa?(>=mujhIb8)ttDsriEk2@ zpNMXTC6Ylh-Z>MTRUGU)+kR0NU`D)q_ zT@h(#a{{3j*U{(`nsHU!GWz;hGTB#YfXUwig-S+SPMESg-_(~&Js_LOJEorwMsh-| zxtJ6UYWKJ1>0uK|3PZ^>B=%YIFihC#8lxK<^&#Ud4zY@B-i83<%U`!&9s+g3!K=&n_-Iv}ZgZyC4~8zy5AQs(XYrCD=8%wld<;%6cp9xHzjV zY#4crDah7AxeKI-c=+yts9pHeUM#gIT3wF#eBLiF)W_KuXs#Ql1H89&U z+n3IB)Ef(hxPs|XJlc$dse|yqH2qywc+)em+Ii)@V9*Z+AGq5kiY))$9+lK6`3l21B(4#+1n z*7lXJ#j9*_JDT=TWo3o~%jPIeng@x7uxG9@#8&4mt%`SIf$>CrXG;y`jkO z;1WM4po|)-`odBxpB89E)QMSs0wd54fKD;aj@nTPB}`+7h!v-GXGn0{^>54q0>J%r zW*TGc>5~>$xJH*wqU@(3#d6EitV;b;R-eyRfT}Oof1