无线与 USB 拓展 / 通信实践发布 内容更新

BLE GATT 配置:让 read、write、notify 和安全门各司其职

用实体维护开关、Secure Connections与认证密钥保护四字节配置,分别处理读取、写响应、通知订阅和重连。

读完能做什么
构建公开状态与敏感配置分开的GATT服务,执行长度/范围/认证/实体门/订阅/重连验收,知道写成功与通知入队各能证明什么。
开始之前
先完成所列前置,能用桥接串口下载IDF工程、断电追线;本页仅指定低压台架。
所需器材
官方S3 v1.1 N8R8、桥数据线、万用表、WP7113ID、330Ω±5%≥0.25W、10kΩ±5%、低压SPST维护开关、支持LE Secure Connections和六位PIN输入的Android手机;私人UART环境
版本组合
固定板:ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8框架 / Python:ESP-IDF5.5.5 / Python3.12.13实际交叉编译器:xtensa-esp-elf-gcc14.2.0 / esp-14.2.0_20260121NimBLE固定gitlink:685675c0128deafdd201c9eb82e61d227364646c安全设置:SC_ONLY=1 / SM_LVL=4 / Legacy与debug keys关闭 / 16B key持久性:sm_bonding=0 / NVS_PERSIST=n / RAM5s+LEDoffLED资料:Kingbright WP7113ID V.14A手机参考:nRF Connect Android4.29.1;实体执行pending
适用范围

仅官方S3 v1.1 N8R8/IDF5.5.5固定NimBLE;Android需支持SC+PIN输入;单连接,RAM配置,不提供默认PIN或JustWorks降级。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/ble-usb-sources.json、ble-usb.md;具体章节已读取并核对本机SDK/managed。版本:固定IDF / 子模块 / 厂家资料验证日期:
构建 / 配置已通过
docs/esp32-evidence/ble-usb-builds.json;实际完整原生构建Exit0,输入/配置/固件与原始log SHA留存。对象:ESP32-S3 / N8R8版本:ESP-IDF5.5.5验证日期:
仿真已通过
docs/esp32-evidence/ble-usb-host.json;纯C应用模型和格式检查,不运行物理BLE/USB。对象:macOS arm64同源主机夹具版本:Apple clang21 ASan/UBSan / Python3.12.13验证日期:
本站实板待验证
examples/esp32/esp32-ble-gatt-config/observations.csv仅表头;未接实板,不声称手机配对/无线接收/键盘效果。对象:指定板/模块/手机或USB主机

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 通信实践

用小LED练习敏感写入,先限定服务边界

这个节点有两类信息:任何连接者可读/订阅的公开状态,以及必须通过安全门才能读写的配置。配置只有通知间隔1..60秒与一个限流LED命令;没有Wi-Fi密码、门锁或大负载控制,也不把公开状态包装成端到端隐私服务。

选择GATT时要分别设计read、write、notify的意义。Read取当前快照;Write Request的成功响应表示服务端接受该次请求;Notify需CCCD订阅,单向通知入队不表示手机应用收到。连接成功只说明建立链路,不自动完成认证或订阅。

参考:[8] 固定NimBLE ATT:特征安全权限执行[9] 固定GATT定义:flags与min_key_size[10] 固定GAP:连接安全状态/订阅/IO事件

02 / 通信实践

维护开关默认关,LED只作低压可观察负载

GPIO5/J1-5经独立10kΩ上拉到3V3,SPST另一端接地。开路HIGH为维护关闭,闭合LOW允许本次配对及敏感写入;程序不依赖内部上拉。GPIO4/J1-4经330Ω±5%≥0.25W接WP7113ID阳极,阴极接GND/J1-22;按厂家外形极性核对,不只看剪短后的脚长。

先全部断电检查支路和短路,再开关开路、桥USB供电。GPIO4是外接单LED,不是板载GPIO38地址灯;LED字段及串口日志是命令,没有光学反馈。不要把此支路换成继电器或风扇后沿用安全结论。

端点连接默认
GPIO5/J1-510k到3V3,同节点SPST到GNDHIGH维护关
GPIO4/J1-4330Ω→WP7113ID阳极,阴极GNDLOW/off
桥USB下载+UART0 115200私人读取本次PIN
原生USB19/20本例不需要不混作桥口

参考:[1] 官方固定板文档[2] Kingbright WP7113ID V.14A:极性、Vf与电流

03 / 通信实践

敏感特征只接受已认证的安全连接

锁定IDF5.5.5所带NimBLE提交,最终配置SC_ONLY=1、SM_LVL=4、SC与security启用,Legacy/debug keys与bond持久化关闭。固定Kconfig帮助有级别编号表述差异,port却把数值直接映射;实际NimBLE syscfg和ble_sm实现按0..4定义。这里明确取4,不以帮助文字里的3冒称Level4;SC_ONLY实现再要求SC和16字节密钥。

配置特征附READ/WRITE_ENC与AUTHEN权限、min_key_size16;callback再次核encrypted、authenticated、key_size16。连接安全变化若失败或较弱则终止。公开状态特征没有这些敏感权限,安全配置不等于广告和公开状态自动保密。

IO固定DISPLAY_ONLY:只有现场维护LOW且收到DISPLAY动作才从硬件随机源为本次尝试生成六位PIN,私人桥口显示,手机输入。没有默认PIN、没有把官方示例123456复制进应用,也不接受Just Works作为MITM认证。若客户端只支持Legacy或无PIN输入,停止实验而非降安全;真实手机握手尚未执行。

sm_bonding=0且无NVS bond保存;RESET丢RAM配置回5s/off。手机可能缓存旧bond,必要时只忘记本台再配对。维护开关关闭不撤销已经认证连接的配置read权限;它封住新的配对DISPLAY与所有config write,不能把关门误写成“立刻断开所有已授权连接”。

代码 / 配置
CONFIG_BT_NIMBLE_SECURITY_ENABLE=y
CONFIG_BT_NIMBLE_SM_SC=y
CONFIG_BT_NIMBLE_SM_SC_ONLY=1
CONFIG_BT_NIMBLE_SM_LVL=4
# CONFIG_BT_NIMBLE_SM_LEGACY is not set
# CONFIG_BT_NIMBLE_SM_SC_DEBUG_KEYS is not set
# CONFIG_BT_NIMBLE_NVS_PERSIST is not set

参考:[3] IDF5.5.5 NimBLE Kconfig:SC/安全/角色[4] IDF5.5.5 port:SM_LVL直接映射[5] 固定NimBLE syscfg:实际安全级别定义[6] 固定NimBLE SMP:SC_ONLY、MITM与key长度[7] 固定NimBLE SC实现[8] 固定NimBLE ATT:特征安全权限执行[9] 固定GATT定义:flags与min_key_size[11] IDF5.5.5 esp_random:硬件随机条件

04 / 通信实践

UUID、长度和订阅按字节约定

service为36fb49a7-8c8a-4c92-9d55-6cf2b877d702,status尾d703,config尾d704。config恰好4B:[版本1, interval uint16LE, LED0/1];合法例01 02 00 01表示2秒/LED命令on。未知版本、错误长度、interval0或61、LED2都拒绝,配置整体保持原值;未实现Write Without Response。

status8B为[版本1, seq uint32LE, interval uint16LE, LED命令]。每个本地定时回调递增seq,即使无人订阅也递增;断开清连接与订阅,重连重新订阅,不回补历史。seq回绕/RESET归零,不是全局消息ID。

ATT权限可能在应用callback之前拒绝请求,手机显示的错误顺序/文本未必等于host模型逐行分支;通过安全门后,0x08授权、0x0D长度、0x13字段值等帮助定位。模型检查通过不等于协议栈已在真实手机上走过同样事件。

特征操作访问与确认
…d703公开statusRead / NotifyRead快照;Notify需CCCD,无应用ACK
…d704 configRead加密+认证+16B key
…d704 configWrite Request上述安全+实体LOW;恰好4B,1..60s/LED0..1

参考:[8] 固定NimBLE ATT:特征安全权限执行[9] 固定GATT定义:flags与min_key_size[10] 固定GAP:连接安全状态/订阅/IO事件

05 / 通信实践

完整主程序与可重建环境

以下main.c与包内逐字一致;纯C模型、host测试、CMake、分区、锁和默认配置一起下载。所有可变config、当前handle、订阅标志及通知定时器都在NimBLE主机事件队列运行,避免另一个任务使用已断开的handle;入队失败只记录,不无限缓存历史。

先按包README在IDF5.5.5/Python3.12.13执行idf.py -B build -D SDKCONFIG=sdkconfig.local build,再核最终安全项和单连接角色,再经桥口flash monitor。实际首轮两处misleading-indentation被-Werror拒绝,保留原日志/源快照;仅修括号换行后重编Exit0,未关闭告警,最终appbin463360B。构建通过不证明手机配对。

代码 / 配置
/* SPDX-License-Identifier: MIT */
#include <string.h>
#include <inttypes.h>
#include "esp_log.h"
#include "esp_random.h"
#include "driver/gpio.h"
#include "nvs_flash.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "nimble/nimble_npl.h"
#include "host/ble_hs.h"
#include "host/ble_sm.h"
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "gatt_model.h"
static const char *TAG="W06";
static const ble_uuid128_t svc_uuid=BLE_UUID128_INIT(0x02,0xd7,0x77,0xb8,0xf2,0x6c,0x55,0x9d,0x92,0x4c,0x8a,0x8c,0xa7,0x49,0xfb,0x36);
static const ble_uuid128_t status_uuid=BLE_UUID128_INIT(0x03,0xd7,0x77,0xb8,0xf2,0x6c,0x55,0x9d,0x92,0x4c,0x8a,0x8c,0xa7,0x49,0xfb,0x36);
static const ble_uuid128_t config_uuid=BLE_UUID128_INIT(0x04,0xd7,0x77,0xb8,0xf2,0x6c,0x55,0x9d,0x92,0x4c,0x8a,0x8c,0xa7,0x49,0xfb,0x36);
static uint16_t status_handle;static uint8_t own_addr_type;static uint32_t sequence;static w06_config_t config={.interval_s=5};static w06_link_t link={.handle=UINT16_MAX};static struct ble_npl_callout timer;
static bool maintenance(void){return gpio_get_level(GPIO_NUM_5)==0;}
static void schedule(void){int rc=ble_npl_callout_reset(&timer,ble_npl_time_ms_to_ticks32(config.interval_s*1000U));if(rc)ESP_LOGE(TAG,"status timer rc=%d",rc);}
static w06_security_t security(uint16_t handle){struct ble_gap_conn_desc d;if(ble_gap_conn_find(handle,&d))return (w06_security_t){0};return (w06_security_t){d.sec_state.encrypted,d.sec_state.authenticated,d.sec_state.key_size};}
static int access(uint16_t conn,uint16_t attr,struct ble_gatt_access_ctxt *ctxt,void *arg){
 (void)attr;const bool is_config=arg!=NULL;
 if(ctxt->op==BLE_GATT_ACCESS_OP_READ_CHR){
  if(!is_config){uint8_t out[8];w06_status(&config,sequence,out);return os_mbuf_append(ctxt->om,out,sizeof(out))?BLE_ATT_ERR_INSUFFICIENT_RES:0;}
  int e=w06_secure(security(conn));if(e)return e;uint8_t out[4]={1,(uint8_t)config.interval_s,(uint8_t)(config.interval_s>>8),config.led?1:0};return os_mbuf_append(ctxt->om,out,sizeof(out))?BLE_ATT_ERR_INSUFFICIENT_RES:0;
 }
 if(!is_config||ctxt->op!=BLE_GATT_ACCESS_OP_WRITE_CHR)return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
 const size_t length=OS_MBUF_PKTLEN(ctxt->om);if(length!=4)return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
 uint8_t bytes[4];if(os_mbuf_copydata(ctxt->om,0,4,bytes))return BLE_ATT_ERR_UNLIKELY;
 int e=w06_write(&config,bytes,sizeof(bytes),security(conn),maintenance());
 if(e){ESP_LOGW(TAG,"WRITE rejected ATT=0x%02x; config unchanged",e);return e;}
 gpio_set_level(GPIO_NUM_4,config.led);schedule();ESP_LOGI(TAG,"WRITE accepted RAM interval=%u LED_command=%u; not observed light",config.interval_s,config.led);return 0;
}
static const struct ble_gatt_svc_def services[]={
 {.type=BLE_GATT_SVC_TYPE_PRIMARY,.uuid=&svc_uuid.u,.characteristics=(struct ble_gatt_chr_def[]){
  {.uuid=&status_uuid.u,.access_cb=access,.flags=BLE_GATT_CHR_F_READ|BLE_GATT_CHR_F_NOTIFY,.val_handle=&status_handle},
  {.uuid=&config_uuid.u,.access_cb=access,.arg=(void*)1,.flags=BLE_GATT_CHR_F_READ|BLE_GATT_CHR_F_WRITE|BLE_GATT_CHR_F_READ_ENC|BLE_GATT_CHR_F_READ_AUTHEN|BLE_GATT_CHR_F_WRITE_ENC|BLE_GATT_CHR_F_WRITE_AUTHEN,.min_key_size=16},
  {0}}},{0}};
static void advertise(void);
static int gap_event(struct ble_gap_event *event,void *arg){
 (void)arg;
 switch(event->type){
 case BLE_GAP_EVENT_CONNECT:
  if(event->connect.status){ESP_LOGW(TAG,"connect failed=%d",event->connect.status);advertise();return 0;}
  w06_connect(&link,event->connect.conn_handle);ESP_LOGI(TAG,"CONNECTED handle=%u; subscription reset",link.handle);
  if(maintenance()){int rc=ble_gap_security_initiate(link.handle);ESP_LOGI(TAG,"security request rc=%d; verify private UART PIN",rc);}return 0;
 case BLE_GAP_EVENT_DISCONNECT:w06_disconnect(&link);ESP_LOGI(TAG,"DISCONNECTED; no pending notifications/history");advertise();return 0;
 case BLE_GAP_EVENT_SUBSCRIBE:
  if(event->subscribe.attr_handle==status_handle){w06_subscription(&link,event->subscribe.conn_handle,event->subscribe.cur_notify);}
  return 0;
 case BLE_GAP_EVENT_ENC_CHANGE:{
  w06_security_t s=security(event->enc_change.conn_handle);ESP_LOGI(TAG,"SECURITY status=%d encrypted=%u authenticated=%u key_bytes=%u",event->enc_change.status,s.encrypted,s.authenticated,s.key_size);
  if(event->enc_change.status||w06_secure(s)){ble_gap_terminate(event->enc_change.conn_handle,BLE_ERR_AUTH_FAIL);}
  return 0;}
 case BLE_GAP_EVENT_PASSKEY_ACTION:{
  if(!maintenance()||event->passkey.params.action!=BLE_SM_IOACT_DISP){ESP_LOGW(TAG,"pairing rejected: maintenance closed or unsupported IO action; no JustWorks acceptance");ble_gap_terminate(event->passkey.conn_handle,BLE_ERR_AUTH_FAIL);return 0;}
  uint32_t number,limit=UINT32_MAX-(UINT32_MAX%1000000U);do{number=esp_random();}while(number>=limit);
  struct ble_sm_io io={.action=BLE_SM_IOACT_DISP,.passkey=number%1000000U};ESP_LOGW(TAG,"PRIVATE PAIRING PIN %06"PRIu32" (this attempt only; do not publish UART log)",io.passkey);
  int rc=ble_sm_inject_io(event->passkey.conn_handle,&io);if(rc)ESP_LOGW(TAG,"inject PIN rc=%d",rc);return 0;}
 case BLE_GAP_EVENT_REPEAT_PAIRING:ESP_LOGW(TAG,"repeat pairing refused; no automatic bond deletion");return BLE_GAP_REPEAT_PAIRING_IGNORE;
 default:return 0;
 }
}
static void advertise(void){
 struct ble_hs_adv_fields fields={0};fields.flags=BLE_HS_ADV_F_DISC_GEN|BLE_HS_ADV_F_BREDR_UNSUP;fields.uuids128=(ble_uuid128_t*)&svc_uuid;fields.num_uuids128=1;fields.uuids128_is_complete=1;
 int rc=ble_gap_adv_set_fields(&fields);if(rc){ESP_LOGE(TAG,"adv_fields rc=%d",rc);return;}
 struct ble_hs_adv_fields scan={0};const char *name="HW-W06";scan.name=(const uint8_t*)name;scan.name_len=strlen(name);scan.name_is_complete=1;rc=ble_gap_adv_rsp_set_fields(&scan);if(rc){ESP_LOGE(TAG,"scan_rsp rc=%d",rc);return;}
 struct ble_gap_adv_params p={0};p.conn_mode=BLE_GAP_CONN_MODE_UND;p.disc_mode=BLE_GAP_DISC_MODE_GEN;rc=ble_gap_adv_start(own_addr_type,NULL,BLE_HS_FOREVER,&p,gap_event,NULL);ESP_LOGI(TAG,"advertise rc=%d",rc);
}
static void timer_event(struct ble_npl_event *event){
 (void)event;++sequence;
 if(link.connected&&link.subscribed){uint8_t out[8];w06_status(&config,sequence,out);struct os_mbuf *om=ble_hs_mbuf_from_flat(out,sizeof(out));if(!om)ESP_LOGW(TAG,"notify allocation failed");else{int rc=ble_gatts_notify_custom(link.handle,status_handle,om);ESP_LOGI(TAG,"NOTIFY queued rc=%d seq=%"PRIu32"; no phone receipt ACK",rc,sequence);}}
 schedule();
}
static void reset(int reason){w06_disconnect(&link);ESP_LOGW(TAG,"host reset=%d",reason);}
static void sync_ready(void){int rc=ble_hs_util_ensure_addr(0);if(!rc)rc=ble_hs_id_infer_auto(0,&own_addr_type);if(rc){ESP_LOGE(TAG,"identity rc=%d",rc);return;}advertise();schedule();}
static void host_task(void *arg){(void)arg;nimble_port_run();nimble_port_freertos_deinit();}
void app_main(void){
 gpio_config_t led={.pin_bit_mask=1ULL<<4,.mode=GPIO_MODE_OUTPUT};ESP_ERROR_CHECK(gpio_config(&led));gpio_set_level(GPIO_NUM_4,0);
 gpio_config_t arm={.pin_bit_mask=1ULL<<5,.mode=GPIO_MODE_INPUT};ESP_ERROR_CHECK(gpio_config(&arm));
 esp_err_t e=nvs_flash_init();if(e!=ESP_OK){ESP_LOGE(TAG,"NVS=%s; no automatic erase",esp_err_to_name(e));return;}
 e=nimble_port_init();if(e!=ESP_OK){ESP_LOGE(TAG,"NimBLE init=%s",esp_err_to_name(e));return;}
 ble_hs_cfg.reset_cb=reset;ble_hs_cfg.sync_cb=sync_ready;ble_hs_cfg.sm_io_cap=BLE_HS_IO_DISPLAY_ONLY;ble_hs_cfg.sm_mitm=1;ble_hs_cfg.sm_sc=1;ble_hs_cfg.sm_sc_only=1;ble_hs_cfg.sm_sec_lvl=4;ble_hs_cfg.sm_bonding=0;
 ble_svc_gap_init();ble_svc_gatt_init();ble_svc_gap_device_name_set("HW-W06");int rc=ble_gatts_count_cfg(services);if(!rc)rc=ble_gatts_add_svcs(services);if(rc){ESP_LOGE(TAG,"GATT register rc=%d",rc);return;}
 ble_npl_callout_init(&timer,nimble_port_get_dflt_eventq(),timer_event,NULL);nimble_port_freertos_init(host_task);
 ESP_LOGI(TAG,"W06 SC-only, authenticated16B key for config, physicalLOW for write; RAM only, no default PIN/bond persistence");
}

参考:[3] IDF5.5.5 NimBLE Kconfig:SC/安全/角色[9] 固定GATT定义:flags与min_key_size[10] 固定GAP:连接安全状态/订阅/IO事件[12] 固定NimBLE NPL:事件队列/callout

06 / 通信实践

按顺序做认证、读回、写入和订阅

以官方Android nRF Connect4.29.1为操作参考,记录真实手机/系统/App版本并处理扫描/连接权限;本站没有手机运行结果。配对PIN只在本台私人UART读取,不把运行日志公开上传;000000也可能是本次随机结果,需保留前导零,不能当成预设密码。

解码工具host/decode_gatt.py只解析复制的4B/8B,不连接、不订阅、不写BLE。README的例字节明确是合成格式;真实观察CSV只填手机实际读取结果。

  1. 维护开关关。扫描HW-W06或service UUID→CONNECT→CLIENT展开;读公开status应有8B。尚未认证读写config应触发安全升级或失败,不能直接改LED。
  2. 断开,维护开关闭合LOW后重连,按本次私人UART六位PIN输入;无提示/失败不降低到Just Works。执行时核SECURITY status=0、encrypted=1、authenticated=1、key_bytes=16。
  3. 读config核4B默认01 05 00 00;对status启用Notify/CCCD,确认读和订阅是两种操作。取消再开订阅并记录。
  4. config用HEX和Write Request写01 02 00 01;读回核2s/LED1,并独立观察LED。试错误长度/边界值后读回不变。
  5. 维护关后合法写应失败,已认证config read仍可能成功;断开重连后重新认证和订阅,无历史回补。
  6. RESET回5s/off;如手机缓存旧bond,仅忘记本设备再配对。结束维护关闭,记录每次拒绝/恢复。

参考:[10] 固定GAP:连接安全状态/订阅/IO事件[13] nRF Connect4.29.1客户端读写/日志[14] Android官方蓝牙权限

07 / 通信实践

让拒绝、重连和恢复都能被观察

至少分组执行未认证、正确PIN、错误PIN/取消、维护关闭、长度3/5B、interval0/61、LED2、订阅开关、断开重连与RESET。拒绝时同时记录ATT错误和读回,不能只用“灯没亮”证明安全门工作。已认证且维护关闭仍可读配置,是本设计范围,不是发现了绕过写门。

无Notify先核CCCD、当前handle与timer/notify rc,不把服务发现当订阅成功;重连不应期待历史。写成功但灯没亮先核命令读回和330Ω/极性,断电修线。配对失败核客户端SC能力和手机旧bond,绝不为方便打开Legacy/debug keys。

故障定位恢复
保护特征拒绝安全状态/key16/实体LOW回现场检查,不降认证
无NotifyCCCD/连接状态/入队rc重订阅;不补历史
PIN/旧bond异常本次PIN、手机缓存、无持久bond仅忘记本台重配对
配置更改未进固件旧sdkconfig优先/版本锁新配置重建,核最终全部安全项

参考:[3] IDF5.5.5 NimBLE Kconfig:SC/安全/角色[6] 固定NimBLE SMP:SC_ONLY、MITM与key长度[8] 固定NimBLE ATT:特征安全权限执行[10] 固定GAP:连接安全状态/订阅/IO事件[13] nRF Connect4.29.1客户端读写/日志

08 / 通信实践

主机能验证权限逻辑,不能替手机证明握手

本轮同源C模型在ASan/UBSan下实际通过68组安全输入、长度0..8、所有版本字节、间隔/LED范围、拒绝不改状态、LE32格式以及错误handle/重连订阅。它不运行ATT/SMP加密、RF或手机UI;加密布尔输入不是密钥交换测试证据。

原生恢复构建、最终配置/源/固件/原log SHA在ble-usb-builds.json,同源命令在ble-usb-host.json,固定源码与读取笔记在ble-usb-sources.json。observations.csv仅表头,物理配对、LED、通知收件和兼容性仍pending。需要USB实体输入时进入下一篇独立工程,不在此公开状态上暗加控制命令。

参考:[6] 固定NimBLE SMP:SC_ONLY、MITM与key长度[8] 固定NimBLE ATT:特征安全权限执行[9] 固定GATT定义:flags与min_key_size

BACK TO THE SOURCE

资料来源与版本说明

按固定官方实现、厂家引脚和实际构建核查;主机夹具仅证明软件边界,指定板/手机/USB实体运行仍未执行。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. 官方固定板文档
  2. Kingbright WP7113ID V.14A:极性、Vf与电流
  3. IDF5.5.5 NimBLE Kconfig:SC/安全/角色
  4. IDF5.5.5 port:SM_LVL直接映射
  5. 固定NimBLE syscfg:实际安全级别定义
  6. 固定NimBLE SMP:SC_ONLY、MITM与key长度
  7. 固定NimBLE SC实现
  8. 固定NimBLE ATT:特征安全权限执行
  9. 固定GATT定义:flags与min_key_size
  10. 固定GAP:连接安全状态/订阅/IO事件
  11. IDF5.5.5 esp_random:硬件随机条件
  12. 固定NimBLE NPL:事件队列/callout
  13. nRF Connect4.29.1客户端读写/日志
  14. Android官方蓝牙权限