Skip to content

BLE网关与子设备协议(v0.8设计稿) ​

本文确定首套实验的通信边界与应用协议,供网关、嵌入式、后端和小程序共同实现。第6–9节是正式接入设计;第11节记录可实测的本地实验,第12节记录后端内部基础。 当前已发布平台仍为v0.7.1,内部代码和建表验证不代表云端桥接已经开放。

1. 首套实验与用途 ​

硬件确定为用户已有的ESP32-S3开发板和WCH CH582官方评估板。S3负责WiFi、MQTT TLS和BLE Central(主动扫描、连接);CH582作为BLE Peripheral(广播、提供GATT服务),不连接WiFi/MQTT。板卡的模组、Flash、RAM、引脚及下载器应根据实物核实,不能由芯片系列名称推断。CH582使用用户提供的CH583EVT公共资料作为参考;CH582与CH583的芯片差异、链接布局和板级引脚需逐项核对,不将CH583默认工程直接当作CH582产品固件。

首个产品定义为实验IO节点:一个可写布尔属性switch_1控制LED,一个只读整数counter记录按键次数;没有真实传感器时不伪造温湿度,不将LED成功等同于继电器触点成功。具体GPIO按板卡确认,按键消抖后计数,范围0至2147483647、饱和不溢出,计数首版重启归零。

项目首版约定
产品标识(拟新增)网关p_ble_gateway_s3;实验节点p_ble_io_lab
网络拓扑一台网关、一块子设备,普通BLE GATT;不做Mesh
控制平台控制LED;板上操作变化回传;成功后报告实际状态
配置子设备report_interval_s为5至3600整数,默认30秒,持久保存
事件与周期状态变化立即上报;counter变化最短1秒合并;周期快照按配置发送
暂不实施子设备OTA、定时、跨网关自动漫游、离线规则执行、历史补传、外部品牌设备自动识别

网关自身沿用现有ESP32配网与认领、TLS及OTA。CH582使用独立BLE业务Profile,不实现乐鑫protocomm Security 2,也不把其应用报文混入ESP32 WiFi配网端点。

2. 分层与身份 ​

小程序复用src/ble/core的适配器、串行GATT队列、MTU及生命周期;新增Profile和业务编解码,不把产品字段写入公共内核。ESP32使用自己的Central实现,共享协议与测试向量,不移植UniApp运行时代码。

标识用途
productId型号与物模型,不作为单机身份
deviceIdPostgreSQL中逻辑设备UUID,HTTP和页面使用
physicalId正式身份为P-256公钥SEC1字节的SHA-256前16字节、32位小写hex;更换密钥改变身份,不是BLE MAC。现有本地实验仍用随机标识,不能直接充当正式身份
BLE地址当前扫描/连接定位,可变化,不作为云端归属凭据
bindingEpoch云端归属代次,32位十六进制;解绑/重新归属更换
bootId当前启动生成的UUID;子设备重启变化
gatewayDeviceId当前唯一授权父网关的逻辑UUID
relationEpoch子设备与网关关联代次,32位十六进制;解除或换网关即失效

网关和子设备是两条独立设备记录,分别有状态、版本和权限。子设备不获得MQTT账号;网关不能使用它的physicalId登录Broker。首版一个子设备只能关联一个网关,必须同一拥有者;接收分享者可控制子设备,但不能添加、迁移、配对或改配置。分享网关不自动分享子设备。

3. 手机先直连、再交接网关 ​

首次添加必须由手机直接连接子设备:物理开启窗口 → 扫描连接 → 读取身份并安全认证 → 登录账号完成认领及必要配置。没有网关也可在蓝牙范围内查看、控制和配置,不把手机直连降为维护功能;首版创建认领和取得授权仍需要互联网,不承诺完全离线首次添加。

用户可以选择同账号的网关,手机安全下发关联信息,云端同步待关联授权,然后手机断开BLE。子设备进入等待网关连接状态,网关连接并认证、完成状态同步后才显示“已通过网关连接”。交接失败保留已完成的认领,显示等待网关连接,允许重试;不要求从头添加或额外创建一条设备记录。不选择网关则仅本地使用。

手机以后仍可直接使用同一份身份、属性与配置;本地操作同样受归属与授权限制,不能因近场连接跳过权限。网关不能自动收编发现的节点,也不能抢占首次手机添加。首版一个活动连接,网关交接与手机本地使用需要双方明确协调。

物理配对、安全与连接交接 ​

“扫描到”和“读取标识”只代表发现,不能直接绑定、控制或冒用身份。BLE链路加密、应用凭据认证和云端账号授权是三道不同的检查。链路加密不证明拥有者身份。

无屏子设备采用物理按键开窗 + 标准BLE SMP LE Secure Connections Just Works + 16字节链路密钥。设备声明NoInputNoOutput,不输入号码,不扫描二维码,不依赖USB串口,不要求MITM认证标志;拒绝Legacy Pairing、短密钥和明文业务。系统可能提供配对确认提示,但不要求用户取得或输入数字码。加密GATT权限不能再同时要求AUTHEN/MITM,否则无输入配对完成后仍会被拒绝。

配对窗口默认120秒,正常状态不接受新的首次接入。窗口内允许附近手机竞争连接;Just Works提供加密而不防首次握手的主动中间人攻击,物理开窗用于限定接入时机,不作为密码学身份认证。这是本项目无屏、免输入接入的明确取舍。旧实验串口六位Passkey方案已撤回,不再作为当前操作步骤。

手机首次直连后完成应用授权与配置,再断开并交给已授权网关;以后手机和网关使用应用凭据确认权限,不能仅凭曾经建立过BLE加密连接取得业务权限。应用凭据建立、持久保存、撤销与网关交接仍按后续接口设计实施,不能将本次加密探针视为账号认领已经实现。当前io.6实验包不保存BLE绑定,只验证实时SC/16字节链路和加密身份读取;正式应用授权不能依赖这个临时探针状态。只允许一个活动连接,不使用全体设备共享的出厂密码。

  • 普通运行:网关持有连接;断开后指数退避1、2、4、8、16、30秒并加抖动,成功后归零。
  • 物理长按进入维护窗口:子设备主动断开网关,广播维护标志;网关发现维护状态后暂停抢连。手机完成后退出或120秒到期,再交回已有授权的网关;未选择网关时不发起网关交接。具体按键/GPIO由板卡确认。
  • 进入维护不清空云端归属;恢复出厂是独立的确认操作,清理本地绑定与配置,并告知用户先在平台解绑。
  • BLE配对成功后,云端仍须批准关联;失败候选不获得转发权限。设备尚未ACTIVE时只允许发现、认证和归属清理,拒绝业务控制。
  • 已撤销但离线的设备不能被远程保证立即擦除本地密钥。云端立即停止转发和接受数据;重新连接时先同步撤销/归属屏障,未完成前不处理业务。失窃网关与固件提取不在实验所证明的防护范围内。

4. 广播与GATT ​

采用新独立128位服务UUID:f6d10001-7c9e-4a19-9c38-02a5840f6e31。设备名BK-IO-xxxx仅辅助用户选择,不能用于鉴权。主广播包含Flags、服务UUID;Scan Response中的Service Data使用相同UUID,数据为protocolMajor:u8=1、flags:u8、model:u16LE=1。flags bit0为可配对、bit1为维护,其余必须为0。完整physicalId连接后读取,避免31字节传统广播超限。

特征UUID属性用途
f6d10002-7c9e-4a19-9c38-02a5840f6e31Read非敏感信息:协议版本、productId、physicalId、固件版本、bootId、能力;加密后读取
f6d10003-7c9e-4a19-9c38-02a5840f6e31Write with responseCentral发送请求分片,必须加密且通过认证
f6d10004-7c9e-4a19-9c38-02a5840f6e31Notify子设备发送回复与报告分片,必须先启用CCCD

信息Read必须支持ATT长读,不假定一次返回整个JSON。手机/网关发现全部必需特征后才继续;缺失时报协议不匹配并给出实际UUID诊断,不忽略校验。先订阅Notify、再交换hello和业务,兼容MTU23,不把MTU517作为接入门槛。

5. 有界传输帧 ​

先编码UTF-8 JSON,再分片。每次ATT写入/通知包含12字节头和一段数据,不依赖隐式字符串结束符;所有多字节整数使用小端。

偏移字段约束
0version:u81;不支持则拒绝
1type:u81请求、2回复、3事件
2..5transferId:u32单连接单方向递增非零;用于重组,不是业务msgId
6..7totalLength:u16完整JSON字节数,1至1024
8fragmentIndex:u8从0开始,严格连续
9fragmentCount:u81至128,与总长和协商分片容量一致
10..11fragmentLength:u16本帧实际JSON片段长度,必须与剩余ATT字节相同

片段容量为min(实际MTU-3-12, 180),连接hello交换双方容量后取较小值并冻结到断线;MTU23时为8字节,最大1024字节消息128片。最后一片允许较短,其余必须填满。首个hello使用8字节片段,因此不依赖协商结果。transferId从1起,溢出前断线重建,不跨连接续传。接收端保留本连接已开始接收的最高编号;错误清理不得抹掉此水位,旧编号/已完成编号再次发送均拒绝,只有断开重建会话才重置。

每方向最多一个重组缓冲,超时5秒清空;总长、片数、序号、长度不合法立即拒绝,断线全部清空。请求未结束不插入另一个请求;事件由发送队列串行化,队列上限8,周期状态可合并,回复不可被状态挤掉。UTF-8非法、重复JSON字段、未知字段、超过深度6及越界类型拒绝。不自增CRC:BLE链路已校验,完整性依赖认证加密;仍要验证长度和结构。

Write成功只代表ATT接收,不代表命令执行。回复超时由应用判断,不能将Notify启用或分片成功当成执行成功。

6. 应用消息与执行规则 ​

hello请求不带归属,只有schemaVersion、msgId、op=hello和params.fragmentBytes;回复含实际容量、身份、bootId、绑定状态与能力。只有hello和归属管理允许在未ACTIVE状态进行,其余结构如下。例中UUID、epoch和UTC秒均为占位,运行时必须从当前状态取得,并计算新期限:

json
{
  "schemaVersion": 1,
  "msgId": "11111111-1111-4111-8111-111111111111",
  "op": "property.set",
  "bindingEpoch": "0123456789abcdef0123456789abcdef",
  "relationEpoch": "abcdef0123456789abcdef0123456789",
  "targetBootId": "22222222-2222-4222-8222-222222222222",
  "expiresAtUtc": 1793203230,
  "params": {"switch_1": true}
}

reply必须包含相同schemaVersion/msgId/op、当前bindingEpoch/relationEpoch/bootId、code及params;例如执行成功params为{"switch_1":true},code为200。事件使用独立msgId和op=property.report,包含当前两个epoch、bootId、递增reportSeq和实际params。服务端按关系、启动、序号拒绝重复/迟到报告;不能只检查UUID格式。bootId变化须先完成启动确认,不能靠自报覆盖当前启动。

op权限与语义
hello认证链路读取身份/能力,不改变归属
binding.prepare/commit/clear仅云端授权的拥有者流程;见下节,不能作为产品公开服务
time.sync已关联网关发送UTC秒与有效时间样本;本次启动未校时前拒绝有有效期的动作
property.getACTIVE后读取实际快照,无副作用
property.setACTIVE、两个epoch、当前boot及30秒内有效期;硬件成功后才200并报告
config.get/set复用平台settings scope、独立revision、全量快照与逐项应用状态;不新增CH582专用后端字段
property.report子设备上报实际状态及周期快照;不能将网关缓存冒充新采样

首版不实现schedules或其他service,能力中不得宣称支持。现有通用校验不会自动变成已支持新操作,接入时须补分发、ACL与契约测试。

错误码为200成功、400结构/范围错误、403未授权、409归属/版本冲突、410过期或启动失配、429忙、500执行/存储失败;失败带有界英文error标识,不带密钥。一次只处理一个会改变状态的请求,第二个返回429而不是无限排队。

子设备保存最近16条本连接的msgId、请求摘要和结果,同msgId同内容返回原结果,不重复动作;同msgId不同内容409。断线清缓存,网关不能重发此前未确认的写操作:先标记结果未知,再读取实际状态。首版仅幂等属性赋值与版本化配置,不开放toggle、脉冲等无法安全重试的动作。命令到期立即丢弃,断网不缓存控制。

7. 云端关联与归属屏障 ​

手机先直连子设备完成安全认领,本地使用不要求已有网关。选择桥接时,网关须以自身身份正常认领并ACTIVE,再由拥有者选择该网关;附近候选由手机识别,网关不能自行认领。云端校验子设备未被他人绑定、当前网关归属、物理窗口及已认证链路证明,创建pending关联;客户端不能直接提交任意childId就取得授权。实验串口初始化不是制造商真伪认证。

子设备也执行持久UNASSIGNED → CLEARING → READY → ACTIVE,与通用运行归属保持相同安全原则:prepare先持久清理屏障,再清旧配置/恢复意图和连接授权;ready后云端事务commit;子设备持久ACTIVE后云端才显示添加成功。断电从CLEARING继续,失败停在屏障内。prepare要带transitionId、旧/新epoch、relationEpoch、物理窗口nonce、当前boot和有效期;commit必须对应已保存transition,不允许另一次请求跳过prepare。具体线级JSON及认证证明必须在CH582最小安全验证后补齐契约测试,本稿不授权先用空证明实现关联。

解绑子设备立即撤销分享、邀请、路由和旧epoch,发clear;离线时显示待设备清理。删除网关先撤销其全部路由,保留子设备记录与历史,显示网关不可用,不级联删除用户设备数据;重新关联须物理窗口和新的relationEpoch。网关不能代子设备跳过READY或自行生成ACTIVE。

8. MQTT桥接与权限隔离 ​

网关只使用自己的sys/{gatewayProductId}/{gatewayPhysicalId}精确前缀,不扩权到sys/+/+/#,也不直接发布子设备普通设备Topic。三组Topic的精确ACL已实现,后端MQTT业务监听尚未启用:

相对网关前缀方向内容
thing/gateway/up上行discovery、reply、property、presence、binding
thing/gateway/down下行云端授权的关联操作、查询、控制、配置
thing/gateway/ack下行上行被平台接受/拒绝;不是设备执行成功

统一外封装为schemaVersion/msgId/gatewayBindingEpoch/gatewayBootId/childDeviceId/relationEpoch/kind/body;未登记发现仅允许kind=discovery,childDeviceId和relationEpoch为null,body只含有界候选信息,不写影子/遥测。其余均不可为空,body为子设备应用报文。下行示意:

json
{
  "schemaVersion": 1,
  "msgId": "33333333-3333-4333-8333-333333333333",
  "gatewayBindingEpoch": "11111111111111111111111111111111",
  "gatewayBootId": "44444444-4444-4444-8444-444444444444",
  "childDeviceId": "55555555-5555-4555-8555-555555555555",
  "relationEpoch": "abcdef0123456789abcdef0123456789",
  "kind": "request",
  "body": {
    "schemaVersion": 1,
    "msgId": "11111111-1111-4111-8111-111111111111",
    "op": "property.set",
    "bindingEpoch": "0123456789abcdef0123456789abcdef",
    "relationEpoch": "abcdef0123456789abcdef0123456789",
    "targetBootId": "22222222-2222-4222-8222-222222222222",
    "expiresAtUtc": 1793203230,
    "params": {"switch_1": true}
  }
}

内外relationEpoch必须相同。后端从认证Topic确定网关,再查询当前关系,不能信任body的设备身份;核对网关和子设备归属、当前boot/期限及原始请求msgId后,进入通用控制/配置/影子服务。普通用户仍调用逻辑设备接口,传输路由根据直连或网关关系选择,不新增“CH582控制接口”。物模型与设备能力取交集。

外层MQTT QoS1、retain=false,拒绝retain桥接消息。每条上行只允许一个子设备,JSON上限4096字节,单子设备正常属性每秒最多一条、控制回复不与周期数据争抢;服务端仍按设备和账号限流。随机候选必须过期清除,不无限记录附近所有蓝牙设备。

9. 在线、存储与故障结果 ​

网关每15秒发送一次独立子设备presence,包含链路状态、最后实际响应时间和boot;子设备每30秒一次GATT存活交互,90秒无响应判离线。主动断开立即上报离线;MQTT断开时云端按网关离线判子设备不可达。恢复须重新认证、确认归属和启动并读取快照,不能仅靠网关上线将子设备标在线。

PostgreSQL持久保存逻辑设备、关联代次、归属与配置;Redis用于有期限等待/去重,不成为唯一归属来源;InfluxDB只写真实数值测量,实验counter默认不展示历史曲线。网关NVS保存授权关系与BLE绑定;CH582保存身份、归属屏障、BLE绑定和settings,写失败不回复接受成功。LED上电默认关闭,不因网关重连恢复未知旧控制。

不在断网期间积压上报或延后执行控制。网关重启先恢复自身启动确认,再逐个恢复子设备;子设备重启生成新boot、重新校时和读配置。恢复出厂改变身份后只能按新发现流程处理,不凭同一BLE MAC覆盖别人的设备。

10. 实现入口与尚需核实范围 ​

实验仓库为hardware/BLE-Gateway-Lab独立子模块,包含gateway、subdevice、protocol和tests。CH582工程使用真实MR3目录及用户导出的CMake,构建产物统一obj;S3使用IDF6.1。io.8/node.6已接SC Just Works、身份/启动计数、实际GATT分片、手机灯控、独占交接和本地网关许可持久日志;用户确认iPhone控制跟手,手机/网关往返及S3查询/开关回复、节点复位恢复均已核对。主机故障注入、ASan/UBSan、实际TS/C互操作和两款构建/烧录通过。正式签名认领与授权的当前实现见第12节;网关MQTT路由和产品持久参数仍未接入,Android未实测;第11节保留本地实验契约,不能以本地许可代替正式云端归属。

编码先完成CH582广播、加密配对、绑定保存、断线恢复及S3 WiFi/TLS并行的最小实验,再冻结配对证明、归属管理JSON和资源预算。本机已取得用户提供的CH583EVT参考包,原参考包头文件CH58xBLE_LIB.h标为V2.10(2024/03/18),当前板上日志为CH58x_BLE_LIB_V2.13,存在SC开关和ECC回调;仅核实文件内容,不代表包为最新、CH582安全配对或工具链已通过。具体摘要与适配边界记录在实验仓库subdevice/SDK.md。参考现有WCH项目只能复用组织方式与测试经验,不能直接承诺二进制兼容。

自动测试覆盖分片边界/MTU23、越界与分配失败、重复命令、伪造关系、撤权、断电屏障、旧boot/epoch、配置落盘失败、网关重连和链路抢占;实物再验证LED/按键闭环、断线恢复以及手机维护交接。容量、续航、距离与多手机体验在实测前不作承诺。执行进度只维护在滴答清单。

实验包的当前烧录入口与数据影响见 hardware/BLE-Gateway-Lab/subdevice/CH582Lab/FLASH-TEST.md(总仓库内文件)。使用者必须区分私密实验种子包与通用固件:本包不上传OSS/Release;DataFlash两个页被用于实验启动计数,主动擦除时需更换本机种子后重建。现阶段使用SC Just Works,验证失败不得退回Legacy或明文读取;S3暂不需要烧录。

11. 当前本地实验:手机控制与S3接管 ​

此节是隔离实验的可执行契约,用于先验证两块开发板及手机。第6–9节带云端epoch、时效、持久归属的正式操作仍为后续接入目标;不能用本实验的物理窗口授权或编译凭据替代账号认领与云端权限。它不开放云端API、MQTT Topic或生产设备控制。

CH582 io.8与小程序node.6使用相同12字节传输头,hello以8字节片段启动,双方根据实际MTU协商8–180字节容量并冻结到断线。hello回复使用旧容量,末片发出后节点才切换。请求为最多512字节的ASCII JSON,禁止转义、数组、重复/未知字段及超过params的嵌套;回复最多384字节。一次一个请求和一个回复,乱序/重复transferId拒绝,重组5秒超时。窗口、启动身份和授权状态都由节点检查。命令写入成功只表示接收,控制页等待匹配回复再更新显示。

请求字段固定为schemaVersion=1、UUIDv4 msgId、op、当前targetBootId和params。下面例子只有本机实验会话已建立后才可执行;bootId必须替换为刚读取的值:

json
{
  "schemaVersion": 1,
  "msgId": "11111111-1111-4111-8111-111111111111",
  "op": "property.set",
  "targetBootId": "22222222-2222-4222-8222-222222222222",
  "params": {"switch_1": true}
}
json
{
  "schemaVersion": 1,
  "msgId": "11111111-1111-4111-8111-111111111111",
  "op": "property.set",
  "bootId": "22222222-2222-4222-8222-222222222222",
  "code": 200,
  "params": {"switch_1": true, "fragmentBytes": 180, "gatewayAssigned": false, "peer": 1},
  "error": ""
}
实验opparams与前提作用
hello仅fragmentBytes整数8–180;已加密返回请求值与设备实际容量的较小值,不授权;后续更改返回409
lab.phone.open空对象;物理120秒窗口、SC加密授予本次手机本地实验会话,不创建云端设备
property.get空对象;已授权会话读取实际PB18状态
property.set仅switch_1布尔;已授权会话赋值控制灯,禁止toggle和未知属性
lab.gateway.assign空对象;手机已授权允许本机配置的这一对实验网关接管
lab.handover空对象;手机已授权且已分配网关回复后关闭手机窗口并断开,开始等待网关
lab.gateway.auth仅token:64位小写hex;已加密、手机窗口关闭、已授权网关校验这一对板子的本机随机凭据,建立网关本地会话

peer=0无会话、1手机、2网关。失败code为400非法值/不支持操作、403未授权或未开窗、409协商容量已冻结、410启动身份失配、500授权日志保存失败;错误结构在GATT层拒绝,不生成伪成功回复。当前没有跨连接msgId结果缓存,不自动重发写操作;超时结果未知时断开,再认证并读取实际状态。

手机页面保留连接,提供开关灯、实际状态查询和交给实验网关三个操作。切后台/退出关闭连接。长按节点3秒会终止当前网关连接并打开手机窗口;网关见到窗口标志后不抢连。手机交接成功只表示节点接受交接,必须由S3串口GATEWAY_READY证明网关也完成应用凭据认证;网页/小程序不能将断开直接显示为网关在线。

实验凭据由tools/setup_gateway_pair.py从本机OS生成32随机字节,为CH582/S3分别创建忽略的私密头文件;不输出密钥、不使用所有设备共用的默认密码,带密钥实验固件不公开。S3还必须配置指定CH582的公开BLE地址,地址仅负责定位。正式网关凭据将由后续平台授权流程下发,不要求最终用户编辑头文件。

当前连接授权仍只属于当前连接;本地网关许可由io.8独立双页DataFlash日志保存(0x200/0x300),正常复位后恢复,灯默认关闭;身份与启动计数仍在0/0x100。日志含配对配置ID、序列、CRC及提交标记,写入/回读成功后才ACK,损坏或配置ID失配拒绝恢复,不回退较旧许可。S3重启在节点许可仍存在时可重新加密/认证并读状态,不重发控制。正式归属持久化、凭据撤销、云端路由、共享权限与MQTT仍未完成。操作步骤、GPIO和烧录入口统一见实验仓库CH582Lab/FLASH-TEST.md与gateway/README.md。

实验验证范围(2026-10-28):用户已确认iPhone能正常开关PB18并执行交接;S3日志已出现GATEWAY_READY、property.get实际状态与property.set开/关灯成功回复。以上不扩展为云端桥接或Android通过;用户再长按切回手机并完成第二次交接,双端日志已确认网关在窗口期不抢连、关闭窗口后重新认证及读取成功;正式持久授权仍需后续实现。

io.8/node.6优化与验证:连接只在hello交换容量,后续操作复用协商值;用户确认iPhone开关灯明显跟手,S3实测开/关回复约275/272ms。实际CH582复位后约12秒恢复S3应用认证与关闭状态读取;不将复位验证等同所有物理掉电中断写入已通过。

平台产品定义准备在yunstack-backend/src/modules/product/catalog/ble-lab-products.ts:p_ble_io_lab仅声明switch_1布尔读写,p_ble_gateway_s3不包含子设备开关属性;通过现行产品校验和通用动作校验。导入此文件不会执行seed、创建设备或修改数据库。两个模板已由显式登记工具加入在线测试库。产品创建DTO接受MQTT/BLE分类,网关云端路由尚未接通;BLE节点不能走普通MQTT设备创建或工厂凭据登记。

12. 正式接入的后端基础 ​

本节对应后端 src/modules/device-gateway。BLE挑战、签名归属与拥有者恢复已接入DeviceClaimModule,提供JWT保护的HTTP接口。网关关联、启动协调、控制与上报处理器仍未注册MQTT监听;本地实验与正式认领验证在手机工具页分开选择,不能将其中一条成功视为整个平台桥接完成。

产品分类与登录边界 ​

创建产品时 transport 只接受 MQTT 或 BLE,省略时沿用MQTT;thingModel.meta.nodeRole 为 device 或 gateway,省略时为device。网关必须是MQTT产品,子设备为BLE/device。创建后不能修改传输类型或角色,避免已有身份因产品编辑改变权限。名称、描述及同分类物模型仍可正常更新。

普通设备创建与空板MQTT凭据登记拒绝BLE产品;EMQX认证和授权也拒绝BLE身份,即使调用者知道该设备的secret。BLE云端控制在正式路由接通前明确拒绝,不尝试把子设备当作直连MQTT设备发布。

设备持有证明 ​

正式子设备需在本机生成并持久保存P-256密钥,私钥不能交给手机或网关。公钥编码为65字节SEC1非压缩格式:04 || X || Y,X/Y均32字节大端,以130位小写hex传输。physicalId取这65字节的SHA-256前16字节。

手机读出公开身份、当前bootId和物理窗口nonce,再由登录账号申请120秒一次性挑战。挑战绑定账号、产品、身份、启动及窗口,手机只能转交;无需用户输入密码或数字码。这个持有证明不提供制造商真伪认证,仍受Just Works首次配对的安全边界约束。

挑战示例(所有身份、nonce和有效期须使用本次实际值,示例不可直接重放):

json
{
  "schemaVersion": 1,
  "challengeId": "11111111-1111-4111-8111-111111111111",
  "userId": "owner-example",
  "productId": "p_ble_io_lab",
  "physicalId": "0123456789abcdef0123456789abcdef",
  "bootId": "22222222-2222-4222-8222-222222222222",
  "windowNonce": "33333333-3333-4333-8333-333333333333",
  "serverNonce": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "expiresAtUtc": 1793203230
}

签名字节为以下九行ASCII,行间单个LF,末尾没有换行;字段按所列顺序,不能直接签JSON序列化结果:

text
BK-BLE-CLAIM-v1
<challengeId>
<userId>
<productId>
<physicalId>
<bootId>
<windowNonce>
<serverNonce>
<expiresAtUtc十进制UTC秒>

设备在确认当前物理窗口和启动仍匹配后,以ECDSA P-256/SHA-256签名,采用IEEE-P1363的64字节 r || s 大端编码,传128位小写hex。服务器核对持久挑战、到期时间、当前账号状态、产品分类、身份停用及设备尚未绑定状态;消费和创建待确认归属转换须在同一事务中完成。回调失败全部回滚,并发消费只有一次成功。

证明消费成功不等于绑定完成,更不能接受手机提交的success标志。事务仅建立pending状态;网络下发在提交之后进行,随后分次验证prepare/ready/commit/active。CH582签名和归属持久化已实现,并通过S3转交的实际BLE报文与隔离数据库完成全流程验证;HTTP协调器已注册公开路由,手机提供实验验证入口;常规添加设备和正式网关页面尚未接入。

数据与网关校验 ​

增量迁移 20261029010000_ble_gateway_foundation 只新增三张表,不回填旧设备或改动业务历史。用户授权后已于2026-10-29应用到在线测试库并核对账本,三张表初始化为空,既有账号/设备/出厂身份数量未变。固定SQL SHA-256为8bd34cd9695273d4e22bfbe7c6799a5e8d9e8fd94890142b9bb709196afe71f7。建表不代表认领接口或云端路由已经启用。

表职责与边界
device_ble_identity产品+physicalId对应公钥、可选逻辑设备及停用状态;不存设备私钥
device_ble_claim_challenge账号、公开身份、启动/窗口、随机挑战、有效期及消费时间;消费不自动生成绑定
device_gateway_relation唯一子设备对应父网关、拥有者快照、双方epoch、子设备boot及关联代次;pending有期限,active必须有确认时间

路由读取使用事务,先锁身份,再按设备ID稳定排序锁设备,最后重读关联。每次检查双方当前拥有者相同且账号启用、运行归属ACTIVE、无待清理、epoch及启动匹配、身份未停用、产品分类正确,不能仅凭关联行status=active授权。网关删除仅级联清理关联,不删除子设备记录。

上行解析只接受reply/property/presence/binding,不接受设计中的discovery或下行request。它要求第8节八个外层字段、当前内外relationEpoch、子设备bindingEpoch/bootId;property必须是property.report并有正安全整数reportSeq,reply必须有明确整数code。拒绝retain、超4096字节及过深/过大结构。Topic认证网关必须与当前关系完全一致,JSON中的声明不能替代它。内部处理器已实现回复关联、序号去重和影子事务,尚未注册到应用模块。命令只在匹配当前路由的子设备执行回复code=200时完成;MQTT发布成功不是执行成功。property/presence共用单调reportSeq,序号与epoch、relationEpoch、bootId一起保存在PostgreSQL影子元数据中,重复、迟到或已撤销消息不刷新在线时间。只读数值测量才写入InfluxDB,灯的布尔状态不生成历史曲线。

验证与继续接入 ​

后端测试使用真实Node P-256签名验证字段替换、伪造签名、过期、重复消费和事务失败;另用随机PostgreSQL临时schema执行实际迁移、实际服务、并发消费及外键删除测试,结束即删除临时schema,不触碰现有业务行。测试入口为后端目录的 node tools/test-ble-gateway-postgres.cjs <后端绝对路径> <显式环境文件路径>,需要对应库的建schema权限;它会连接该环境指定的数据库,仅适合获准的测试环境,不作为生产升级命令。

后续先完成手机正式认领入口的真机验证,再接入S3自身MQTT认领、关联确认、子设备控制/上报与撤权回归。HTTP认领接口与精确Topic ACL已具备,不能据此跳过后续验证。任何一项尚未接通时,不让页面把手机断开或BLE连接成功显示为云端绑定完成。

正式挑战与READY/ACTIVE回复已有独立纯C签名字节编码器(实验仓库protocol/c),与后端实际P-256验证交叉测试通过。CH582现已接入独立身份公钥、物理窗口随机nonce和claim.info/claim.prove操作;私钥由本机独立随机种子、芯片ID及配置域派生,不复用BLE临时密钥。使用厂商uECC确定性签名(RFC6979)、SEC1公钥和P1363签名;厂商本机小端摘要/标量与云端大端字节显式转换。真实板端签名经Node验证通过,正常复位公钥不变、bootId改变。签名只证明当前物理窗口持有该设备,尚不代表云端认领完成。S3实验网关已增加独立USB网络诊断任务,实板DHCP、HTTPS200和8883证书/主机名通过,网络检查同时8次BLE控制往返约206–429ms,重启恢复验证通过。串口配置不替代最终用户配网;MQTT设备认证和正式桥接仍待接入,操作入口见实验仓库gateway/README.md。

BLE认领协调适配器现已在代码中复用通用归属事务:有效设备挑战创建未归属的pending设备;带设备签名的READY后才提交owner/epoch,带签名的ACTIVE后才完成。真实临时PostgreSQL验证覆盖完整流程、跨账号、阶段替换与停用身份;普通MQTT认领回归保持通过。适配器已注册HTTP路由,手机实验入口通过设备签名逐步确认;用户实际发起正式认领才会创建云端设备归属,不在启动或扫描时自动创建。

protocol/c/ble_binding.c提供独立双页持久化内核,先写CLEARING再执行清理,READY写入成功后才允许回复,ACTIVE落盘后才完成。写入失败、损坏或配置身份不一致保持关闭;CLEARING正常重启继续清理。该内核与厂商Flash适配已接入板端binding.prepare/commit,主机覆盖实际适配及签名回复,CH582构建通过;正式归属后拒绝旧实验控制操作。实际CH582 claim.2与S3 gateway.net.2已完成隔离的真实BLE认领:云端一次性挑战、设备签名、持久READY、云端提交归属、持久ACTIVE及云端完成确认。测试只使用临时数据库schema,结束后删除;板子已重新烧录恢复未认领状态,不留下虚构账号归属。手机的正式认领验证入口已准备;日常产品入口和云端MQTT桥接仍未完成。

网关授权和控制处理器的实现边界 ​

拥有者为自己已激活的子设备与网关申请120秒关联授权。每次授权产生新的relationEpoch和独立令牌;令牌由服务器现有回调密钥按域分隔HMAC派生,不作为数据库明文保存。子设备必须签名确认双方逻辑ID、归属代次、启动ID、关联代次、有效期和令牌摘要。只有子设备确认后,已认证网关从自己的Topic确认,关联才进入active。分享用户不能建立或撤销关联,当前子设备拥有者即使在旧网关已解绑后仍可撤销。

MQTT授权代码已限定:仅MQTT类型且nodeRole=gateway的设备能够发布自己的thing/gateway/up、订阅自己的thing/gateway/down和thing/gateway/ack。网关不能直接使用子设备Topic。接收端拒绝retained重放;当前HTTP授权回调没有接收原始publish的retain参数,因此不宣称Broker已阻止所有携带retain标志的原始发布。

内部控制处理器每个子设备只允许一个等待中的控制,超时最多30秒,不自动重放。解绑子设备或父网关会取消等待。上下行均验证双方当前归属和启动,影子写入在同一事务持锁完成,避免撤权与遥测写入交错。网关签名授权、MQTT监听和控制处理器目前仍是未注册的内部实现,不代表线上可控。

S3提供有界USB诊断通道,供隔离测试转交claim.prove、binding.prepare和binding.commit;不会为附近节点自动创建账号。串口使用中断接收缓冲,避免长报文在轮询间隔丢失。CH582签名编码器手工转换UTC秒,避免精简newlib不支持长整数格式导致云端验签失败。两项问题均由实板复现并修复。

小程序认领编排器严格匹配每阶段身份,HTTP回复丢失时查询服务器已提交状态,取消后忽略迟到结果。验证页提供独立云端认领选项;只构建本地Build,不对临时实验自动上传微信开发版本。后续还须接上日常产品页面、网关自身认领、正式MQTT和节点自动启动签名确认。

节点重启确认与授权落盘 ​

子设备重启生成新bootId后,普通状态上报不能自行更新服务器保存的bootId。内部启动协调器由当前已认证父网关申请30秒挑战:绑定旧bootId、目标bootId、双方ID/epoch、当前父网关bootId、relationEpoch及随机serverNonce。服务器用现有回调密钥作独立域HMAC封签,子设备另以身份私钥签名;两者都通过后,在同一归属事务中更新运行记录、活动归属转换和网关关系。旧启动证明不能覆盖较新的启动,重复确认同一启动可安全重试。确认先置离线,随后新启动的有效上报才恢复在线。

签名字节为ASCII行,依次为BK-BLE-BOOT-v1、childDeviceId、childBindingEpoch、previousBootId、bootId、gatewayDeviceId、gatewayBindingEpoch、gatewayBootId、relationEpoch、serverNonce、expiresAtUtc;单LF分隔,末尾无LF,ECDSA/P1363编码与认领一致。当前协调器只在隔离测试使用,MQTT启动入口和板端启动签名操作仍待接入。

正式网关许可已接入CH582 gateway.grant:当前ACTIVE身份及物理窗口内验证关联字段,保存双方逻辑ID/epoch、relationEpoch与32字节独立令牌,然后返回P-256签名证明。响应只包含令牌SHA-256摘要,不能回显令牌。授权摘要按原始32字节计算,不对hex文本计算。该证明的ASCII行依次为BK-BLE-GRANT-v1、childDeviceId、childBindingEpoch、childBootId、gatewayDeviceId、gatewayBindingEpoch、gatewayBootId、relationEpoch、expiresAtUtc、tokenHash。

CH582正式授权使用独立双页日志(Flash适配偏移0x600/0x700),与身份、归属和早期本地实验许可分离;页中使用二进制ID/代次/令牌,不依赖结构体内存布局。带CRC、递增序号、最后提交标志和回读校验。归属清理屏障先持久化,再清除授权;任何写入失败不确认成功,当前连接也不得继续使用旧的内存许可。发生无法恢复的损坏时需要开发期重新初始化,不为旧实验状态增加兼容旁路。

真实S3→CH582授权写入、子设备签名、隔离数据库验签和双方确认关联已通过。测试完删除随机schema并清理实验板临时归属,190404字节写入校验通过。此结果没有使用真实账号,也不表示S3已使用正式令牌接入MQTT;该后半链路继续实现。C与TypeScript的授权/启动签名字节跨语言验证覆盖32位以上UTC值和安全整数边界,主机存储故障测试和实际固件编译通过。

当前连接认证与正式控制帧 ​

独立授权写入以后,每条BLE连接必须重新认证。设备产生30秒有效、一次性随机nonce,网关以本关联32字节令牌计算HMAC-SHA-256;令牌不作为认证报文直接发送。MAC覆盖ASCII行BK-BLE-GATEWAY-AUTH-v1、childDeviceId、childBindingEpoch、当前子设备bootId、gatewayDeviceId、gatewayBindingEpoch、relationEpoch、nonce、utcNow,末尾无LF。utcNow为网关已校准的UTC秒,也受MAC保护,用于本连接的指令有效期判断。任何失败均消费挑战,断开或更换授权清除连接认证,认证时间基线最多使用30分钟。

正式property.set保留平台的schemaVersion/msgId/op/bindingEpoch/relationEpoch/targetBootId/expiresAtUtc/params八字段,不另造实验控制格式。CH582当前产品适配只写布尔属性switch_1,检查ACTIVE归属、已认证连接、两种代次、启动和最多30秒有效期;回复包含实际状态和明确结果码。16项有界本启动去重缓存只清除已过期项;相同ID改变内容返回409,缓存全部占用返回429,不淘汰仍有效指令制造重放。跨启动报文由bootId拒绝。

实板已验证:S3转交云端派生令牌的挑战应答、正式控制帧开/关PB18及状态返回。这个测试经过USB诊断转交,不经过MQTT Broker。独立网关连接管理、MQTT桥接及断线后的正式启动确认仍待接通,不能宣称整条线上控制链路已验收。

手机认领与中断恢复的当前入口 ​

以下路径均在/api/v1下,需要有效访问Token,账号来自JWT,不能提交ownerId替代。

方法与路径用途
POST /ble-claims/challenges未归属节点的一次性设备挑战
POST /ble-claims/challenges/:id/proof消费设备签名并建立待确认归属
POST /ble-claims/binding转交设备签名READY或ACTIVE
GET /ble-claims/challenges/:id查询本次新认领的权威状态
POST /ble-claims/resume/challenges已归属节点的当前拥有者申请新物理窗口证明;未归属返回null
POST /ble-claims/resume/:id验证恢复签名并返回原归属commit命令
GET /ble-claims/resume/:id查询恢复后的实际归属确认状态

恢复挑战沿用第12节完整9字段挑战和P-256签名格式,但serverNonce由服务器按独立恢复域、当前epoch/旧boot/转换状态计算HMAC封签。只有当前启用的拥有者和同一设备公钥可使用;分享用户、旧拥有者、旧启动证明、解绑或清理中的设备均拒绝。它不重新建档、不更换epoch,也不清除配置;设备重启时仅在新物理窗口签名通过后更新启动确认,再要求设备重做持久commit回复。手机退出或最后一步网络中断后,可以重新开窗恢复原确认,不需要删除设备重新添加。

本地Build验证页为“我的→子设备安全验证”,构建标记0.8.0-node.7,选择“扫描并验证云端认领”。需先登录,长按PB4约3秒开窗;不输入WiFi、密码或二维码。页面只有在服务器确认ACTIVE后才显示认领成功。该步骤会创建当前账号的真实BLE设备,原本地实验灯控随后停止适用;正式手机控制凭据和网关产品流程继续开发。普通“扫描实验设备”仍只用于未正式认领的本地探针。此处不是正常产品添加入口。

两个产品模板p_ble_io_lab与p_ble_gateway_s3已登记在线测试库,只新增产品定义,不自动登记身份、创建设备或分配拥有者。维护工具为后端目录node tools/register-ble-lab-products.cjs /绝对路径/环境文件;默认只读核对,显式--apply才创建缺失模板,现有类型或模型冲突立即停止,绝不覆盖产品模型。后端已构建是前提;新环境恢复工具时先运行相关隔离测试。