Files
story/firmware/echoear-platform-agent/main/ble_setup.c
T

389 lines
22 KiB
C
Raw Normal View History

#include "ble_setup.h"
#include "ble_cloud.h"
#include "ble_name.h"
#include "ble_wifi.h"
#include "device_reset.h"
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include "esp_netif.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "host/ble_hs.h"
#include "host/util/util.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"
#include "mbedtls/platform_util.h"
#include "nvs.h"
#include "volc_hal.h"
#define MAX_FRAME 2050
#define NO_CONN BLE_HS_CONN_HANDLE_NONE
static const char *TAG="ble_setup";
// BLE UUID byte arrays are little endian.
#define BLE_UUID(last) BLE_UUID128_INIT(0x00,0x61,0x5e,0x4f,0x82,0x7d,0x31,0x9c,0x2d,0x4a,0x4b,0x6f,last,0x20,0x7a,0x8e)
static const ble_uuid128_t service_uuid=BLE_UUID(0),rx_uuid=BLE_UUID(1),tx_uuid=BLE_UUID(2);
static uint16_t tx_handle;
static uint8_t own_address_type;
static atomic_uint connection=NO_CONN, epoch;
static atomic_bool subscribed, enabled;
static atomic_bool window_closed;
static int64_t window_end, fragment_start, fragment_last;
static uint8_t rx_buffer[MAX_FRAME];
static size_t rx_len;
static struct ble_npl_callout watchdog;
typedef struct { unsigned generation; uint16_t conn; uint8_t kind; size_t len; int64_t started; uint8_t data[MAX_FRAME]; } record_t;
typedef struct { unsigned generation; int status; } ack_t;
static QueueHandle_t records,acks;
static SemaphoreHandle_t state_mutex;
static TaskHandle_t binding_task;
typedef struct { uint32_t version; char ssid[33],password[64],token[44],press[37]; } job_t;
static job_t job;
typedef enum { IDLE,RECEIVED,WIFI_CONNECTING,BINDING,SUCCESS,FAILED } state_t;
static state_t state;
static const char *state_names[]={"idle","received","wifi_connecting","binding","success","failed"};
static const char *failure_message="";
static int64_t confirm_until;
static bool recover_saved_wifi, timeout_handled;
static void show(const char *text)
{
volc_hal_context_t *ctx=volc_get_global_hal_context();
if(ctx&&ctx->display_handle) volc_hal_display_set_content(ctx->display_handle,VOLC_DISPLAY_OBJ_SUBTITLE,VOLC_DISPLAY_TEXT,(void*)text);
}
static void set_state(state_t value,const char *message)
{
xSemaphoreTake(state_mutex,portMAX_DELAY);state=value;failure_message=message?message:"";xSemaphoreGive(state_mutex);
const char *display[]={"Bluetooth setup","Tap to confirm WiFi","Connecting WiFi","Checking account","WiFi setup saved","Setup failed; retry"};
const char *reset_display[]={"Bluetooth setup","Tap to ERASE settings and WiFi","Connecting WiFi","Authorizing factory reset","Settings erased; restarting","Reset failed; retry"};
show(job.version==3?reset_display[value]:display[value]); ESP_LOGI(TAG,"state=%s",state_names[value]);
}
static esp_err_t pending_save(bool keep)
{
nvs_handle_t nvs;esp_err_t err=nvs_open("ble_setup",NVS_READWRITE,&nvs);if(err!=ESP_OK)return err;
err=keep?nvs_set_blob(nvs,"pending",&job,sizeof(job)):nvs_erase_key(nvs,"pending");
if(err==ESP_ERR_NVS_NOT_FOUND)err=ESP_OK;
if(err==ESP_OK)err=nvs_commit(nvs);
nvs_close(nvs);return err;
}
static esp_err_t complete_save(void)
{
nvs_handle_t nvs;esp_err_t err=nvs_open("ble_setup",NVS_READWRITE,&nvs);if(err!=ESP_OK)return err;
err=nvs_set_u8(nvs,"completed",1);
if(err==ESP_OK)err=nvs_set_u8(nvs,"protocol",2);
if(err==ESP_OK)err=nvs_erase_key(nvs,"pending");
if(err==ESP_ERR_NVS_NOT_FOUND)err=ESP_OK;
if(err==ESP_OK)err=nvs_commit(nvs);
nvs_close(nvs);return err;
}
static esp_err_t save_wifi(void)
{
nvs_handle_t nvs;esp_err_t err=nvs_open("wifi",NVS_READWRITE,&nvs);if(err!=ESP_OK)return err;
err=nvs_set_str(nvs,"ssid",job.ssid);
if(err==ESP_OK)err=nvs_set_str(nvs,"password",job.password);
if(err==ESP_OK)err=nvs_set_u8(nvs,"setup_requested",0);
if(err==ESP_OK)err=nvs_commit(nvs);
nvs_close(nvs);return err;
}
static void binding_worker(void *arg)
{
bool resume=arg!=NULL;
for(;;){
if(!resume)ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
resume=false;
// The confirmation event and all retry inputs reach flash before HTTP.
if(pending_save(true)!=ESP_OK){set_state(FAILED,"设备保存失败,请重试");continue;}
set_state(WIFI_CONNECTING,NULL);
esp_err_t wifi_err=ble_wifi_connect(job.ssid,job.password);
if(wifi_err!=ESP_OK){
ESP_LOGE(TAG,"Wi-Fi setup failed: %s",esp_err_to_name(wifi_err));
const char *message=wifi_err==ESP_ERR_NO_MEM?"设备内存不足,未能启动 Wi-Fi,请重试":
wifi_err==ESP_ERR_TIMEOUT?"Wi-Fi 连接失败,请检查 2.4 GHz 网络和密码":"设备 Wi-Fi 初始化失败,请重试";
if(pending_save(false)==ESP_OK)set_state(FAILED,message);
else set_state(BINDING,"设备保存失败,请重新启动恢复任务");
continue;
}
set_state(BINDING,NULL);
for(;;){
if(!ble_wifi_connected()&&ble_wifi_connect(job.ssid,job.password)!=ESP_OK){
vTaskDelay(pdMS_TO_TICKS(10000));continue;
}
bool resetting=job.version==3;
esp_err_t err=resetting?device_reset_from_setup(job.press):job.token[0]?ble_cloud_bind(job.token,job.press):ble_cloud_check_bound(job.press);
if(err==ESP_OK){
// Retain the pending record until both commits complete; a power
// cut here replays the same token, never creates another binding.
if(!resetting&&(save_wifi()!=ESP_OK||complete_save()!=ESP_OK)){vTaskDelay(pdMS_TO_TICKS(5000));continue;}
set_state(SUCCESS,NULL);vTaskDelay(pdMS_TO_TICKS(15000));esp_restart();
}
if(err==ESP_ERR_INVALID_RESPONSE){
if(pending_save(false)==ESP_OK)set_state(FAILED,resetting?"重置未完成,请先在原账号解绑;通话或升级结束后再试":job.token[0]?"平台拒绝绑定,请查询账号归属或重新申请":"设备已解绑,请重新搜索并绑定账号");
break;
}
// Transport/5xx/429 uncertainty is not a failed binding. The same
// token and persisted press ID are retried with a fresh signed nonce.
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
}
bool ble_setup_confirm(void)
{
if(!atomic_load(&enabled))return false;
xSemaphoreTake(state_mutex,portMAX_DELAY);
if(state==RECEIVED&&esp_timer_get_time()>=confirm_until){
state=FAILED;failure_message="确认超时,请重新发送配网信息";
}
if(esp_timer_get_time()>=window_end&&(state==IDLE||state==FAILED)){
xSemaphoreGive(state_mutex);return false;
}
bool accept=state==RECEIVED&&esp_timer_get_time()<confirm_until;
if(accept){ble_random_uuid(job.press);state=WIFI_CONNECTING;}
xSemaphoreGive(state_mutex);
if(accept)xTaskNotifyGive(binding_task);
return true;
}
static void recover_wifi_after_timeout(void)
{
if(!atomic_load(&enabled)||timeout_handled||esp_timer_get_time()<window_end)return;
xSemaphoreTake(state_mutex,portMAX_DELAY);
if(state==RECEIVED&&esp_timer_get_time()>=confirm_until){
state=FAILED;failure_message="确认超时,请重新发送配网信息";
}
bool idle=state==IDLE||state==FAILED;
xSemaphoreGive(state_mutex);
if(!idle)return; // A confirmed/persisted job continues even after BLE closes.
timeout_handled=true;
if(!recover_saved_wifi)return; // First-time setup has no known-good Wi-Fi.
nvs_handle_t nvs;size_t pending_len=0;
if(nvs_open("ble_setup",NVS_READONLY,&nvs)!=ESP_OK)return;
esp_err_t err=nvs_get_blob(nvs,"pending",NULL,&pending_len);nvs_close(nvs);
if(err!=ESP_ERR_NVS_NOT_FOUND)return; // Preserve confirmed work or uncertain storage.
err=nvs_open("wifi",NVS_READWRITE,&nvs);
if(err==ESP_OK){
err=nvs_set_u8(nvs,"setup_requested",0);
if(err==ESP_OK)err=nvs_commit(nvs);
nvs_close(nvs);
}
if(err==ESP_OK){
ESP_LOGI(TAG,"Setup window expired; restoring saved Wi-Fi");
show("Setup timed out; restoring WiFi");esp_restart();
}else{
ESP_LOGE(TAG,"Cannot restore saved Wi-Fi: %s",esp_err_to_name(err));
show("WiFi restore failed; restart");
}
}
static bool valid_password(const char *p)
{
size_t len=strlen(p);if(len<8||len>63)return false;
for(size_t i=0;i<len;i++)if((uint8_t)p[i]<32||(uint8_t)p[i]>126)return false;
return true;
}
static const char *json_string(cJSON *root,const char *key)
{cJSON *v=cJSON_GetObjectItemCaseSensitive(root,key);return cJSON_IsString(v)?v->valuestring:NULL;}
static esp_err_t endpoint(uint32_t session,const uint8_t *input,ssize_t len,uint8_t **output,ssize_t *outlen,void *arg)
{
if(len<=0||len>2048||memchr(input,0,len))return ESP_ERR_INVALID_ARG;
// Require a single JSON object; cJSON alone otherwise accepts trailing data.
char *text=calloc(1,len+1);if(!text)return ESP_ERR_NO_MEM;memcpy(text,input,len);
if(strstr(text,"\\u0000")){mbedtls_platform_zeroize(text,len);free(text);return ESP_ERR_INVALID_ARG;}
const char *end=NULL;cJSON *root=cJSON_ParseWithOpts(text,&end,true);const char *cmd=json_string(root,"cmd");
if(!cJSON_IsObject(root)||!cmd){cJSON_Delete(root);mbedtls_platform_zeroize(text,len);free(text);return ESP_ERR_INVALID_ARG;}
bool valid=true;unsigned count=0;cJSON *item;
cJSON_ArrayForEach(item,root){
count++;if(!item->string||!cJSON_IsString(item))valid=false;
for(cJSON *prev=root->child;prev!=item;prev=prev->next)if(prev->string&&item->string&&!strcmp(prev->string,item->string))valid=false;
if(item->string&&strcmp(item->string,"cmd")&&strcmp(item->string,"wifi_ssid")&&strcmp(item->string,"wifi_password")&&strcmp(item->string,"bind_token"))valid=false;
}
cJSON *result=cJSON_CreateObject();
if(!valid||!result)goto invalid;
if(!strcmp(cmd,"get_info")&&count==1){cJSON_AddStringToObject(result,"device_id",ble_factory_id());cJSON_AddStringToObject(result,"model",ble_factory_model());cJSON_AddNumberToObject(result,"protocol_version",2);cJSON_AddBoolToObject(result,"factory_reset_supported",true);}
else if((!strcmp(cmd,"get_status")&&count==1)||((!strcmp(cmd,"configure")||!strcmp(cmd,"factory_reset"))&&count==4)){
const char *error=NULL;xSemaphoreTake(state_mutex,portMAX_DELAY);
if(state==RECEIVED&&esp_timer_get_time()>=confirm_until){state=FAILED;failure_message="确认超时,请重新发送配网信息";}
if(!strcmp(cmd,"configure")||!strcmp(cmd,"factory_reset")){
bool resetting=!strcmp(cmd,"factory_reset");
const char *ssid=json_string(root,"wifi_ssid"),*password=json_string(root,"wifi_password"),*token=json_string(root,"bind_token");
if(!ssid||!ssid[0]||strlen(ssid)>32||!password||!valid_password(password)||!token||(token[0]&&(strlen(token)!=43||strspn(token,"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")!=43)))error="配网字段格式不正确";
else if(resetting&&token[0])error="恢复出厂不能携带绑定凭据";
else if(job.version==(resetting?3:2)&&state!=FAILED&&!strcmp(job.token,token)){
if(strcmp(job.ssid,ssid)||strcmp(job.password,password))error="同一绑定凭据不能修改配网内容";
}else if(state==RECEIVED||state==WIFI_CONNECTING||state==BINDING||state==SUCCESS)error="设备已有配网任务,请先查询结果";
else if(esp_timer_get_time()>=window_end)error="蓝牙配网窗口已关闭,请重新长按开启";
else{
mbedtls_platform_zeroize(&job,sizeof(job));job.version=resetting?3:2;
strcpy(job.ssid,ssid);strcpy(job.password,password);strcpy(job.token,token);
state=RECEIVED;failure_message="";confirm_until=esp_timer_get_time()+60000000;
show(resetting?"Tap to ERASE settings and WiFi":"Tap screen to confirm WiFi");
}
}
cJSON_AddStringToObject(result,"status",error?"failed":state_names[state]);
cJSON_AddStringToObject(result,"operation",job.version==3?"factory_reset":"configure");
if(error||state==FAILED)cJSON_AddStringToObject(result,"message",error?error:failure_message);
xSemaphoreGive(state_mutex);
}else goto invalid;
*output=(uint8_t*)cJSON_PrintUnformatted(result);*outlen=*output?strlen((char*)*output):0;
cJSON_Delete(root);cJSON_Delete(result);mbedtls_platform_zeroize(text,len);free(text);
return *output?ESP_OK:ESP_ERR_NO_MEM;
invalid:
cJSON_Delete(root);cJSON_Delete(result);mbedtls_platform_zeroize(text,len);free(text);return ESP_ERR_INVALID_ARG;
}
static void terminate_link(void)
{unsigned handle=atomic_load(&connection);if(handle!=NO_CONN)ble_gap_terminate(handle,BLE_ERR_REM_USER_CONN_TERM);}
static int gatt_access(uint16_t conn,uint16_t attr,struct ble_gatt_access_ctxt *ctx,void *arg)
{
if(ctx->op!=BLE_GATT_ACCESS_OP_WRITE_CHR||!atomic_load(&subscribed))return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
uint16_t len=OS_MBUF_PKTLEN(ctx->om);
if(!len||len>20||rx_len+len>MAX_FRAME){terminate_link();return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;}
if(!rx_len)fragment_start=esp_timer_get_time();
fragment_last=esp_timer_get_time();
if(os_mbuf_copydata(ctx->om,0,len,rx_buffer+rx_len)){terminate_link();return BLE_ATT_ERR_UNLIKELY;}
rx_len+=len;if(rx_len<2)return 0;
size_t expected=2+rx_buffer[0]+((size_t)rx_buffer[1]<<8);
if(expected<3||expected>MAX_FRAME||rx_len>expected){terminate_link();return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;}
if(rx_len==expected){
record_t *record=calloc(1,sizeof(*record));if(!record)return BLE_ATT_ERR_INSUFFICIENT_RES;
record->generation=atomic_load(&epoch);record->conn=conn;record->kind=2;record->len=rx_len;record->started=fragment_start;memcpy(record->data,rx_buffer,rx_len);
rx_len=0;fragment_start=fragment_last=0;
if(xQueueSend(records,&record,0)!=pdTRUE){free(record);terminate_link();return BLE_ATT_ERR_INSUFFICIENT_RES;}
}
return 0;
}
static const struct ble_gatt_svc_def services[]={
{.type=BLE_GATT_SVC_TYPE_PRIMARY,.uuid=&service_uuid.u,.characteristics=(struct ble_gatt_chr_def[]){
{.uuid=&rx_uuid.u,.access_cb=gatt_access,.flags=BLE_GATT_CHR_F_WRITE},
{.uuid=&tx_uuid.u,.access_cb=gatt_access,.flags=BLE_GATT_CHR_F_INDICATE,.val_handle=&tx_handle}, {0}}},{0}};
static int gap_event(struct ble_gap_event *event,void *arg);
static void advertise(void)
{
if(esp_timer_get_time()>=window_end)return;
struct ble_hs_adv_fields adv={.flags=BLE_HS_ADV_F_DISC_GEN|BLE_HS_ADV_F_BREDR_UNSUP,.uuids128=(ble_uuid128_t*)&service_uuid,.num_uuids128=1,.uuids128_is_complete=1};
char name[VOICELINK_BLE_NAME_CAPACITY];voicelink_ble_name(name,ble_factory_model(),ble_factory_id());
struct ble_hs_adv_fields scan={.name=(uint8_t*)name,.name_len=strlen(name),.name_is_complete=1};
int rc=ble_gap_adv_set_fields(&adv);
if(!rc)rc=ble_gap_adv_rsp_set_fields(&scan);
if(rc){ESP_LOGE(TAG,"Cannot configure BLE advertising: %d",rc);return;}
struct ble_gap_adv_params params={.conn_mode=BLE_GAP_CONN_MODE_UND,.disc_mode=BLE_GAP_DISC_MODE_GEN};
rc=ble_gap_adv_start(own_address_type,NULL,5000,&params,gap_event,NULL);
if(rc)ESP_LOGE(TAG,"Cannot start BLE advertising: %d",rc);
else ESP_LOGI(TAG,"BLE advertising started");
}
static void queue_link(uint8_t kind,uint16_t conn,unsigned gen)
{
record_t *r=calloc(1,sizeof(*r));if(!r){terminate_link();return;}
r->kind=kind;r->conn=conn;r->generation=gen;
if(xQueueSend(records,&r,0)!=pdTRUE){free(r);terminate_link();}
}
static int gap_event(struct ble_gap_event *event,void *arg)
{
switch(event->type){
case BLE_GAP_EVENT_CONNECT:
if(event->connect.status){advertise();break;}
atomic_store(&connection,event->connect.conn_handle);atomic_store(&subscribed,false);rx_len=0;fragment_start=0;
queue_link(0,event->connect.conn_handle,atomic_fetch_add(&epoch,1)+1);break;
case BLE_GAP_EVENT_DISCONNECT:
queue_link(1,event->disconnect.conn.conn_handle,atomic_load(&epoch));
atomic_store(&connection,NO_CONN);atomic_store(&subscribed,false);rx_len=0;fragment_start=0;advertise();break;
case BLE_GAP_EVENT_SUBSCRIBE:if(event->subscribe.attr_handle==tx_handle)atomic_store(&subscribed,event->subscribe.cur_indicate);break;
case BLE_GAP_EVENT_NOTIFY_TX:
if(event->notify_tx.indication&&event->notify_tx.status!=0){ack_t ack={.generation=atomic_load(&epoch),.status=event->notify_tx.status};xQueueSend(acks,&ack,0);}break;
case BLE_GAP_EVENT_ADV_COMPLETE:advertise();break;
default:break;
}
return 0;
}
static void watchdog_tick(struct ble_npl_event *event)
{
int64_t now=esp_timer_get_time();
if(fragment_start&&(now-fragment_start>45000000||now-fragment_last>10000000))terminate_link();
if(now>=window_end&&!atomic_exchange(&window_closed,true)){
if(ble_gap_adv_active())ble_gap_adv_stop();
terminate_link();
}
ble_npl_callout_reset(&watchdog,ble_npl_time_ms_to_ticks32(1000));
}
static void on_sync(void)
{
int rc=ble_hs_util_ensure_addr(0);
if(!rc)rc=ble_hs_id_infer_auto(0,&own_address_type);
if(rc){ESP_LOGE(TAG,"Cannot initialize BLE address: %d",rc);return;}
ble_npl_callout_init(&watchdog,nimble_port_get_dflt_eventq(),watchdog_tick,NULL);
ble_npl_callout_reset(&watchdog,ble_npl_time_ms_to_ticks32(1000));advertise();
}
static void host_task(void *arg){nimble_port_run();nimble_port_freertos_deinit();}
static bool send_frame(record_t *r,const uint8_t *data,size_t len)
{
uint8_t frame[MAX_FRAME];if(!len||len>2048)return false;
frame[0]=len&255;frame[1]=len>>8;memcpy(frame+2,data,len);
ack_t ack;while(xQueueReceive(acks,&ack,0)==pdTRUE){}
int64_t start=r->started;
for(size_t at=0;at<len+2;at+=20){
if(r->generation!=atomic_load(&epoch)||r->conn!=atomic_load(&connection)||!atomic_load(&subscribed)||esp_timer_get_time()-start>45000000)return false;
size_t size=len+2-at;if(size>20)size=20;
struct os_mbuf *mbuf=ble_hs_mbuf_from_flat(frame+at,size);if(!mbuf)return false;
if(ble_gatts_indicate_custom(r->conn,tx_handle,mbuf))return false;
if(xQueueReceive(acks,&ack,pdMS_TO_TICKS(10000))!=pdTRUE||ack.generation!=r->generation||ack.status!=BLE_HS_EDONE)return false;
}
return true;
}
static void protocol_worker(void *arg)
{
for(;;){
recover_wifi_after_timeout();
record_t *r=NULL;if(xQueueReceive(records,&r,pdMS_TO_TICKS(1000))!=pdTRUE)continue;
if(r->kind==2&&r->generation==atomic_load(&epoch)&&r->conn==atomic_load(&connection)){
uint8_t *out=NULL;ssize_t len=0;
esp_err_t err=endpoint(r->generation,r->data+2,r->len-2,&out,&len,NULL);
if(err!=ESP_OK||!send_frame(r,out,len))terminate_link();
if(out){mbedtls_platform_zeroize(out,len);free(out);}
}
mbedtls_platform_zeroize(r,sizeof(*r));free(r);
}
}
bool ble_setup_start_if_needed(void)
{
if(ble_cloud_init()!=ESP_OK)return false;
nvs_handle_t nvs;uint8_t requested=0;char ssid[33]={0};size_t len=sizeof(ssid);
if(nvs_open("wifi",NVS_READONLY,&nvs)==ESP_OK){nvs_get_u8(nvs,"setup_requested",&requested);nvs_get_str(nvs,"ssid",ssid,&len);nvs_close(nvs);}
bool pending=false;uint8_t completed=0,protocol=0;
if(nvs_open("ble_setup",NVS_READONLY,&nvs)==ESP_OK){len=sizeof(job);pending=nvs_get_blob(nvs,"pending",&job,&len)==ESP_OK&&len==sizeof(job)&&(job.version==1||job.version==2||job.version==3);nvs_get_u8(nvs,"completed",&completed);nvs_close(nvs);}
if(nvs_open("ble_setup",NVS_READONLY,&nvs)==ESP_OK){nvs_get_u8(nvs,"protocol",&protocol);nvs_close(nvs);}
if(!pending)mbedtls_platform_zeroize(&job,sizeof(job));
if(completed&&protocol==2&&!requested&&ssid[0]&&!pending)return false;
recover_saved_wifi=completed&&protocol==2&&ssid[0]&&!pending;
// Refuse malformed persisted records instead of sending unbounded strings.
if(pending&&(!memchr(job.ssid,0,sizeof(job.ssid))||!memchr(job.password,0,sizeof(job.password))||job.token[43]||job.press[36]||(job.version==3&&job.token[0])))return false;
state_mutex=xSemaphoreCreateMutex();records=xQueueCreate(4,sizeof(record_t*));acks=xQueueCreate(4,sizeof(ack_t));
if(!state_mutex||!records||!acks)return false;
// Reserve the small internal Wi-Fi buffers before BLE allocations.
// This does not connect or send data; a physical tap is still required.
esp_err_t wifi_err=ble_wifi_prepare();
if(wifi_err!=ESP_OK)ESP_LOGW(TAG,"Wi-Fi prepare deferred: %s",esp_err_to_name(wifi_err));
if(pending)state=BINDING;
if(xTaskCreateWithCaps(binding_worker,"ble_bind",16384,pending?(void*)1:NULL,5,&binding_task,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT)!=pdPASS)return true;
if(xTaskCreateWithCaps(protocol_worker,"ble_protocol",12288,NULL,5,NULL,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT)!=pdPASS)return true;
window_end=esp_timer_get_time()+300000000;
ESP_LOGI(TAG,"Before BLE init: internal_free=%u largest=%u",(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL),(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
if(nimble_port_init()!=ESP_OK){show("Bluetooth init failed; restart");return true;}
ble_svc_gap_init();ble_svc_gatt_init();
if(ble_gatts_count_cfg(services)||ble_gatts_add_svcs(services)){
show("Bluetooth service failed; restart");return true;
}
char name[VOICELINK_BLE_NAME_CAPACITY];voicelink_ble_name(name,ble_factory_model(),ble_factory_id());ble_svc_gap_device_name_set(name);
ble_hs_cfg.sync_cb=on_sync;nimble_port_freertos_init(host_task);atomic_store(&enabled,true);
if(!pending)set_state(IDLE,NULL);
ESP_LOGI(TAG,"BLE initialized; waiting for host sync: %s model=%s",name,ble_factory_model());return true;
}