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
@@ -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);