389 lines
22 KiB
C
389 lines
22 KiB
C
#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,¶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;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;
|
||
|
|
}
|