feat(provisioning): add mini-program BLE Wi-Fi setup

This commit is contained in:
chenjintao
2026-08-13 15:44:18 +08:00
parent 5711c26a9a
commit 841fe32c49
21 changed files with 1776 additions and 41 deletions
+1
View File
@@ -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. |
@@ -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");
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;
}
@@ -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.
@@ -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);
@@ -6,9 +6,13 @@
#include "control_plane.h"
#include <string.h>
#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",
"Boot STA connect failed; preserve credentials and enter BLE provisioning immediately: %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();
}
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;
}
@@ -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);
+1
View File
@@ -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
+55
View File
@@ -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);
/**
@@ -0,0 +1,410 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "domain.h"
#include <stdio.h>
#include <string.h>
#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;
}
@@ -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);
+3
View File
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+50
View File
@@ -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);
@@ -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.
@@ -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]),
@@ -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);
@@ -0,0 +1,616 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "platform.h"
#include <string.h>
#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,
&parameters, 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
@@ -6,6 +6,9 @@
#include "platform.h"
#include <string.h>
#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};
@@ -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<char *>(config.sta.ssid),
ssid,
sizeof(config.sta.ssid) - 1U);
std::strncpy(reinterpret_cast<char *>(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);
+24
View File
@@ -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
+16 -2
View File
@@ -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"