Appearance
设备发现
设备发现是绑定的第一步,目的是获取以下信息:
| 字段 | 用途 |
|---|---|
serialNum0 | 设备唯一标识,用于BLE连接和云端注册 |
productId | 产品标识,用于APP展示产品图片、名称,以及加载特定的绑定流程 |
productId 的作用
APP根据 productId 可以:
- 显示对应的产品图片和名称
- 加载特定的绑定引导页面(如操作指引动画)
- 显示特定的用户提示(如"请在设备上按下确认键")
- 展示该产品的用户协议和隐私政策
关于序列号的详细说明,请参考 数据结构 - 设备。
关于产品ID的详细说明,请参考 数据结构 - 产品与规格。
发现途径
| 途径 | 数据来源 | 适用场景 |
|---|---|---|
| BLE扫描 | 蓝牙广播数据 | 有蓝牙模块的设备 |
| 二维码 | 扫描设备上的二维码 | 设备上印有二维码 |
| NFC | 读取设备NFC标签 | 有NFC标签的设备 |
二维码
设备二维码包含以下信息:
https://example.com/bind?sn0={serialNum0}&pid={productId}| 参数 | 必填 | 说明 |
|---|---|---|
| sn0 | 是 | 电路板序列号 serialNum0 |
| pid | 是 | 产品ID deviceProductId |
APP扫描二维码后,解析出 serialNum0,然后通过BLE扫描找到对应设备进行绑定。
NFC
NFC标签使用NDEF格式存储,包含一条URI记录,格式与二维码相同。
为什么不直接包含userSecret?
二维码和NFC仅包含设备标识,不包含 userSecret,原因:
- 安全性:二维码可能被拍照泄露,NFC可能被复制
- 一致性:所有途径都通过指令获取
userSecret,流程统一 - 可控性:设备可以控制何时返回
userSecret(如需物理确认)
BLE广播
具有蓝牙模块的设备通过BLE广播向APP传递设备标识和状态信息。
适用范围
本节仅适用于具有蓝牙模块的设备。无蓝牙模块的设备可通过NFC、二维码等其他方式发现。
BLE广播数据结构
广播数据包含两部分:
| 数据类型 | AD Type | 内容 | 用途 |
|---|---|---|---|
| Service UUID | 0x07 | 32位自定义UUID | 设备状态、产品ID |
| Manufacturer Data | 0xFF | 序列号 | 设备唯一标识 |
Service UUID
广播使用32位或128位UUID,其中前4字节为自定义数据,采用小端序:
| 位 | 位数 | 取值范围 | 字段名 | 说明 | 取值 |
|---|---|---|---|---|---|
| [0] | 1 | 0~1 | bound | 绑定状态 | 0=未绑定,1=已绑定 |
| [1] | 1 | 0~1 | sleeping | 休眠状态 | 0=唤醒,1=休眠 |
| [2:13] | 12 | 0~4095 | deviceProductId | 产品ID | 后台系统分配 |
| [14:23] | 10 | - | 预留 | 保留位 | 0 |
| [24:31] | 8 | - | protocolId | 协议标识 | 0xFD(253) |
关于 deviceProductId 的详细说明,请参考 数据结构 - 产品与规格。
产品ID
deviceProductId 标识设备所属的产品,APP可据此展示对应的图标和名称。同一产品下的不同规格(型号)使用相同的产品ID。
ID分配规则:
| 范围 | 用途 | 说明 |
|---|---|---|
| 0 | 保留 | 不使用 |
| 1~99 | 公共产品ID | 无产品管理后台时使用,按设备类型分配 |
| 100~4095 | 平台分配 | 有产品管理后台时,由平台统一分配 |
公共产品ID:
| ID | 设备类型 |
|---|---|
| 1 | 门锁 |
| 2 | 网关 |
休眠状态
设备在待机时单片机可能处于休眠状态,此时BLE芯片仍在广播但无法响应连接请求。
APP扫描到 sleeping=1 的设备时,应提示用户唤醒设备(如触摸键盘),待广播中 sleeping 变为 0 后再发起连接。
Manufacturer Data
Manufacturer Data(厂商自定义数据)用于携带设备序列号,格式如下:
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 2 | Company ID | 蓝牙SIG分配的公司标识,小端序 |
| 2 | 1 | 数据类型 | 0x01 = 序列号数据 |
| 3 | 1 | serialNum0长度 | 电路板序列号的字节长度 (8-16) |
| 4 | 8-16 | serialNum0 | 电路板序列号(ASCII) |
| 4+n | 1 | serialNum1长度 | 经销商序列号的字节长度 (0或8-16) |
| 5+n | 0-16 | serialNum1 | 经销商序列号(ASCII),长度为0时省略 |
关于序列号的详细说明,请参考 数据结构 - 设备。
数据示例
serialNum0 = "ABC123456789", serialNum1 = ""(无经销商序列号):
Company ID: 0x1234 (示例)
数据类型: 0x01
serialNum0长度: 0x0C (12)
serialNum0: "ABC123456789"
serialNum1长度: 0x00serialNum0 = "ABC123456789", serialNum1 = "XYZ98765":
Company ID: 0x1234 (示例)
数据类型: 0x01
serialNum0长度: 0x0C (12)
serialNum0: "ABC123456789"
serialNum1长度: 0x08 (8)
serialNum1: "XYZ98765"扫描与解析
扫描过滤
APP扫描时应同时检查 Service UUID 和 Manufacturer Data:
javascript
function parseServiceUUID(uuid) {
// UUID格式: FDxxxxxx-0000-1000-8000-00805F9B34FB
const hex = uuid.substring(0, 8)
const value = parseInt(hex, 16)
return {
bound: (value >> 0) & 0x1,
sleeping: (value >> 1) & 0x1,
deviceProductId: (value >> 2) & 0xFFF, // 12位产品ID
protocolId: (value >> 24) & 0xFF
}
}
function parseManufacturerData(data) {
// data: ArrayBuffer
const view = new DataView(data)
const companyId = view.getUint16(0, true) // 小端序
const dataType = view.getUint8(2)
if (dataType !== 0x01) return null
const sn0Len = view.getUint8(3)
const sn0 = String.fromCharCode(...new Uint8Array(data, 4, sn0Len))
let sn1 = ''
if (4 + sn0Len < data.byteLength) {
const sn1Len = view.getUint8(4 + sn0Len)
if (sn1Len > 0) {
sn1 = String.fromCharCode(...new Uint8Array(data, 5 + sn0Len, sn1Len))
}
}
return {
companyId,
serialNum0: sn0,
serialNum1: sn1
}
}
// 过滤可绑定的设备
function isBindableDevice(serviceUUID, manufacturerData) {
const uuid = parseServiceUUID(serviceUUID)
const mfr = parseManufacturerData(manufacturerData)
return uuid.protocolId === 0xFD
&& uuid.deviceProductId !== 0
&& uuid.bound === 0
&& uuid.sleeping === 0
&& mfr !== null
&& mfr.serialNum0.length >= 8
}通过序列号查找设备
当通过二维码或NFC获取到 serialNum0 后,可扫描BLE设备并匹配:
javascript
function findDeviceBySerialNum(targetSerialNum0, devices) {
return devices.find(device => {
const mfr = parseManufacturerData(device.manufacturerData)
return mfr && mfr.serialNum0 === targetSerialNum0
})
}扫描建议
- 仅显示
bound=0的设备用于绑定 - 提示用户唤醒
sleeping=1的设备 - 通过
protocolId=0xFD过滤非本协议设备 - 使用
serialNum0作为设备唯一标识,而非MAC地址
为什么不用 MAC 地址作为设备标识?
iOS 系统不会向 APP 暴露蓝牙设备的真实 MAC 地址,且同一设备在不同 iOS 手机上会得到不同的标识。详见 iOS 蓝牙设备标识说明。
下一步
发现设备并获取 serialNum0 后,连接设备并调用 绑定指令 完成绑定流程。
