#include "ble_setup.h" #include "ble_cloud.h" #include "ble_name.h" #include "ble_wifi.h" #include "device_reset.h" #include #include #include #include #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){ 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;i126)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,¶ms,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;atgeneration!=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; }