feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
@@ -0,0 +1,388 @@
#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;
}