# 蓝牙配网与账号绑定:完整精简版 日期:2026-09-13|状态:待实现的协议约定 手机/电脑与设备使用 **BLE GATT** 通信;设备联网后通过 **HTTPS** 联系平台。业务数据使用 **UTF-8 JSON**。 本文按新的精简约定实现,不与之前详细草案的报文混用。核心配网内容只有三项:**Wi-Fi 名称、Wi-Fi 密码、绑定令牌**。加密、分包由蓝牙通信模块统一处理。 ## 1. 搜索并选择设备 1. 用户先登录平台,长按设备配网键 3 秒。 2. 设备进入配网模式,开启约 5 分钟蓝牙广播,名称为 `EchoEar-000001`,末尾编号与外壳标识一致。 3. 用户点击平台的“添加设备”。页面按配网 Service UUID 筛选,浏览器弹出设备选择框。 4. 用户选择编号对应的设备,页面连接设备的 GATT 服务。 网页通过用户手机/电脑的蓝牙发现设备,设备此时不需要先连接 Wi-Fi。网页须通过 HTTPS 打开,并使用支持 Web Bluetooth 的浏览器。[浏览器约束](https://developer.chrome.com/docs/capabilities/bluetooth) 精简版约定以下固定 UUID;它们是本方案新拟定的值,前端与固件必须一致: | 用途 | UUID | 操作 | |---|---|---| | 配网服务 | `8e7a2000-6f4b-4a2d-9c31-7d824f5e6100` | 用于发现并连接设备 | | 接收入口 RX | `8e7a2001-6f4b-4a2d-9c31-7d824f5e6100` | 页面 Write with response,设备接收 | | 应答入口 TX | `8e7a2002-6f4b-4a2d-9c31-7d824f5e6100` | 设备 Indicate,页面订阅接收 | 设备广播包放配网服务 UUID,完整名称放扫描响应包。UUID 表示服务类型,不表示设备归属;名称相同也不能视为同一台设备。 ## 2. 连接并读取设备信息 页面连接后先订阅 TX,使用设备二维码中的配网凭据完成安全握手,再通过 RX 发送: ```json {"cmd":"get_info"} ``` 设备通过 TX 返回: ```json {"device_id":"KP01-000000000001","model":"ECHOEAR-S3"} ``` device_id 是设备出厂写入、后台已登记的唯一编号;model 是设备型号。页面核对二维码与返回编号,显示设备型号和编号。这里只传设备身份信息,不传用户账号。 安全握手由前端、固件共用的通信模块完成;配网凭据属于设备,不是平台账号密码。当前 ESP32-S3 工程沿用乐鑫 Protocomm Security 2 patch 1,底层约定见第 8 节。 ## 3. 平台申请绑定令牌 页面把选中的 device_id 提交给后台: ```http POST /api/v1/web/ble-bind-tokens ``` ```json {"device_id":"KP01-000000000001"} ``` 后台从当前登录态取得用户账号,检查设备是否允许绑定,返回: ```json {"bind_token":"后台生成的一次性绑定令牌"} ``` bind_token 同时对应“当前账号 + 目标设备 + 本次绑定申请”,固定有效期 5 分钟。由后台生成 32 个随机字节并编码为 Base64URL,不需要另加 session_id 或 setup_nonce。用户不填写这个字段,页面自动取得。 每次开始新一轮配网,后台生成新令牌,并在同一事务中使该账号、该设备此前未提交的申请失效;已经成功的绑定不受影响。页面只有在尚未发送配置或旧任务明确失败时才申请新令牌;旧任务结果不明时先查询,不能自动重开申请。 ## 4. 页面发送 Wi-Fi 和绑定信息 用户输入设备要连接的 Wi-Fi 名称和密码,点击“开始配网”。页面通过 RX 发送: ```json { "cmd":"configure", "wifi_ssid":"MyHome", "wifi_password":"example-password", "bind_token":"后台生成的一次性绑定令牌" } ``` cmd 表示这条消息要做什么;其余三项就是配网内容。因为 RX 共用,保留 cmd 用来区分“读取信息、提交配置、查询进度”。 | 字段 | 必须 | 约定 | |---|---|---| | cmd | 是 | 固定为 `configure` | | wifi_ssid | 是 | Wi-Fi 名称,UTF-8 编码后 1~32 字节 | | wifi_password | 是 | 本版支持 WPA2-Personal 密码,8~63 个可打印 ASCII 字符 | | bind_token | 是 | 第 3 步后台返回的原值,32 字节的 Base64URL 编码,无填充 | 本版按当前 EchoEar 的 2.4GHz Wi-Fi 使用;开放网络、企业网络和 WPA3-only 不在本版范围内。Wi-Fi 密码只通过加密蓝牙给设备,不上传后台;平台登录密码也不发给设备。 设备完整收到并校验后,保存待处理任务,通过 TX 返回: ```json {"status":"received"} ``` received 只代表已收到配置。设备屏幕提示“确认配网与绑定”,用户在 60 秒内短按确认后开始执行;确认关联当前这份配置,由设备内部记录,不增加消息字段。 ## 5. 设备连接 Wi-Fi,并向后台绑定 设备固件执行以下步骤: 1. 解析 wifi_ssid、wifi_password、bind_token,完成本次实体确认。 2. 调用 Wi-Fi 驱动连接路由器,状态变为 wifi_connecting。 3. 取得 IP 后保存可用 Wi-Fi 配置,状态变为 binding。 4. 使用设备自身的认证凭据,通过 HTTPS 请求平台绑定接口。 ```http POST /api/v1/device/ble-bind ``` ```json {"bind_token":"设备通过蓝牙收到的绑定令牌"} ``` 设备编号由既有设备认证请求头提供,body 不重复传。认证沿用平台已有 DEVREQ1;仅提交一个 device_id 字符串不算设备认证。 后台验证令牌、设备身份、账号权限及当前归属,在同一个事务中建立绑定并消费令牌,然后返回: ```json {"status":"success"} ``` 设备收到后台成功结果并保存后,才把本地状态改为 success。设备已绑定其他账号时后台拒绝,不覆盖原归属。AI 服务激活独立处理,不阻塞账号绑定完成。 ## 6. 页面查询进度,结束配网 页面每隔约 2 秒通过 RX 发送: ```json {"cmd":"get_status"} ``` 设备通过 TX 返回当前状态;例如: ```json {"status":"wifi_connecting"} ``` 失败时多一个 message 字段: ```json {"status":"failed","message":"Wi-Fi连接失败,请检查网络名称和密码"} ``` | status | 页面含义 | |---|---| | idle | 尚未收到配置 | | received | 已收到,等待设备上的确认 | | wifi_connecting | 正在连接 Wi-Fi | | binding | 正在确认账号绑定结果 | | success | 网络可用、后台绑定成功、设备结果已保存 | | failed | 本次未完成,展示 message | 读取设备信息、提交配置、查询状态必须串行:前一个应答处理完再发送下一个请求。设备只返回请求对应的应答,不插入主动进度推送。配置执行在设备后台进行,查询状态不会重新触发配网。 页面看到 success 后显示“设备添加成功”并断开。设备成功后最多保留蓝牙 30 秒用于读取结果,然后关闭配网服务;关闭蓝牙不影响 Wi-Fi 和绑定。 ## 7. 必要的失败处理 - **同一份配置重复发送:** bind_token 同时作为任务标识。相同令牌、相同 Wi-Fi 参数只返回原任务状态,不重复执行;相同令牌携带不同参数则返回 failed,提示先结束原申请。设备一次只处理一个任务,新令牌不能覆盖进行中的任务。 - **蓝牙断开:** 不取消已经受理的任务。窗口仍有效时可以重新握手并查询当前任务;页面也可向下面的后台接口查询原申请。 - **设备已提交但没有收到回包:** 使用原令牌重试 `/api/v1/device/ble-bind`。后台先检查同一申请是否已成功,再判断未提交令牌是否过期;同一有效绑定返回原成功结果,不建立第二条绑定。 - **暂时无法确认云端结果:** 保持 binding 并恢复查询,不直接报“绑定失败”,也不发起第二次绑定。页面等待超过 2 分钟时显示“仍在确认,可稍后查看”。 - **Wi-Fi 连接失败:** 返回 failed 和原因,保留原可用配置。用户修改网络后创建新的绑定申请再提交。 - **云端成功但设备保存失败:** 后台绑定保留,设备恢复本地保存;页面提示“已绑定,设备正在同步”,不要求重新绑定。 页面查询云端申请: ```http POST /api/v1/web/ble-bind-status ``` ```json {"bind_token":"本次绑定令牌"} ``` 后台只允许原申请账号查询,返回 status=pending / success / failed;failed 时带 message。这里的 success 只表示后台绑定已完成,不能证明设备已经保存结果。令牌放请求正文,不放 URL。 三个 HTTP 接口均为拟新增。上文 HTTP 响应示例展示业务 data 内容,实际外层沿用平台现有响应封装;网页接口使用现有登录 Cookie 和 CSRF 校验,设备接口使用现有设备认证。后台不返回或记录 Wi-Fi 密码,设备日志不输出密码与令牌。 ## 8. 给开发的底层约定 以下由通信模块实现,页面业务只调用“读信息、配网、查状态”: - 使用 ESP-IDF v5.5.1 **Protocomm Security 2 patch 1** 完成双向认证及加密;二维码保存设备配网凭据,单设备云端认证密钥仍留在设备和后台。握手 protobuf、密钥和 nonce 规则直接遵循官方实现,不另设计密码算法。[官方组件](https://docs.espressif.com/projects/esp-idf/en/v5.5.1/esp32s3/api-reference/provisioning/protocomm.html)、[固定版本实现](https://github.com/espressif/esp-idf/blob/v5.5.1/components/protocomm/src/security/security2.c) - 为了知道完整消息在哪里结束,每条传输消息只加 **3 字节头**:`类型(1字节)+ 内容长度(2字节,小端)+ 内容`。类型 0 是官方握手 protobuf,类型 1 是加密后的完整 JSON;内容长度为 1~2048 字节,业务密文最少 17 字节。握手完成前拒绝类型 1,完成后拒绝类型 0。 - 先加密完整 JSON,再加头,最后每次最多 20 字节顺序写入 RX。头只在整条消息开头出现一次;设备按长度收齐后才解密处理。TX 应答用相同格式分片。前端等待每次写入应答,设备等待每次 Indication 确认后再发下一片。 - 字段名按本文固定;不接受重复字段、未知字段或非法 UTF-8。最长业务 JSON 为 2032 字节,以留出 16 字节认证标签;不要把一个 GATT 分片当作完整 JSON。 - 同连接一次只允许一个未完成请求;10 秒无收发进展或 45 秒仍未收齐完整消息就断开,重新握手。半条消息和旧加密会话不续用;业务结果通过原 bind_token 恢复。两轮握手消息的发送顺序与加密计数直接交给官方安全层。 - 设备接收回调只组包并投递任务,Wi-Fi 和 HTTPS 在后台执行;完整业务请求应在 5 秒内开始返回应答。蓝牙和 Wi-Fi 模块均不解释“用户账号”,账号归属由后台令牌记录决定。 设备只在配网窗口内接收新配置,已受理任务可在窗口结束后继续完成。业务层不再单列配网窗口随机号;安全握手仍保留其内部随机数,令牌到期、一次消费、当前任务约束及实体确认仍须执行。 当前工作区设备尚需实现该蓝牙服务,本文件没有修改固件或发布后台接口。联调至少验证:正常成功、密码错误、重复提交、蓝牙断开、后台回包丢失、已绑定设备拒绝覆盖。