feat(provisioning): add mini-program BLE Wi-Fi setup
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user