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