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