本文基于当前项目里的实际实现整理,重点不是照搬官方文档,而是把这个仓库真实发出去的字节、对应的 type/subtype、常见负载格式和交互顺序讲清楚,方便你迁移到其他平台。
相关实现文件:
blufi/constants.jsblufi/blufi.jspages/device/device.js
当前项目固定使用如下 UUID:
- Service:
0000FFFF-0000-1000-8000-00805F9B34FB - Write Characteristic:
0000FF01-0000-1000-8000-00805F9B34FB - Notify Characteristic:
0000FF02-0000-1000-8000-00805F9B34FB
移植到其他平台时,本质流程不变:
- 连接 BLE 设备
- 找到 Service
- 找到写特征值和通知特征值
- 打开 Notify
- 按 BLUFI 包格式写入数据
- 在 Notify 回调里解析设备返回的数据
blufi.js 中 buildPacket() 生成的数据格式如下:
[Byte0][Byte1][Byte2][Byte3][Payload...]
各字节含义:
Byte0:type + subtype组合字节Byte1:frameCtrl,当前项目发送时固定写死为0x00Byte2:sequence,序列号,0~255循环递增Byte3:dataLength,负载长度Payload: 负载内容
也就是:
packet = [typeSubtype, 0x00, sequence, payload.length, ...payload]
注意两点:
- 当前项目发送侧没有真正启用加密,也没有主动附加校验字节。
- 页面里有“加密通讯”入口,但实际提示为“暂不支持加密模式通讯”,因此跨平台先按明文模式打通最稳。
项目中 Byte0 的构造方式为:
Byte0 = (subtype << 2) | type
其中:
type只占低 2 位subtype左移 2 位后放入高 6 位
| type | 含义 |
|---|---|
0x00 |
控制帧 CTRL |
0x01 |
数据帧 DATA |
收到设备返回的第 1 个字节后,可按下面逻辑解析:
type = byte0 & 0x03
subtype = byte0 >> 2
控制帧 type = 0x00,因此首字节为:
byte0 = (subtype << 2) | 0x00
| 控制命令 | subtype | Byte0 |
|---|---|---|
ACK |
0x00 |
0x00 |
SET_SEC_MODE |
0x01 |
0x04 |
SET_WIFI_OPMODE |
0x02 |
0x08 |
CONN_TO_AP |
0x03 |
0x0C |
DISCONN_FROM_AP |
0x04 |
0x10 |
GET_WIFI_STATUS |
0x05 |
0x14 |
DEAUTHENTICATE_STA |
0x06 |
0x18 |
GET_VERSION |
0x07 |
0x1C |
DISCONNECT_BLE |
0x08 |
0x20 |
GET_WIFI_LIST |
0x09 |
0x24 |
数据帧 type = 0x01,因此首字节为:
byte0 = (subtype << 2) | 0x01
| 数据命令/上报 | subtype | Byte0 |
|---|---|---|
NEG |
0x00 |
0x01 |
STA_BSSID |
0x01 |
0x05 |
STA_SSID |
0x02 |
0x09 |
STA_PASSWD |
0x03 |
0x0D |
SOFTAP_SSID |
0x04 |
0x11 |
SOFTAP_PASSWD |
0x05 |
0x15 |
SOFTAP_MAX_CONN_NUM |
0x06 |
0x19 |
SOFTAP_AUTH_MODE |
0x07 |
0x1D |
SOFTAP_CHANNEL |
0x08 |
0x21 |
USERNAME |
0x09 |
0x25 |
CA |
0x0A |
0x29 |
CLIENT_CERT |
0x0B |
0x2D |
SERVER_CERT |
0x0C |
0x31 |
CLIENT_PRIV_KEY |
0x0D |
0x35 |
SERVER_PRIV_KEY |
0x0E |
0x39 |
WIFI_REP |
0x0F |
0x3D |
REPLY_VERSION |
0x10 |
0x41 |
WIFI_LIST |
0x11 |
0x45 |
ERROR_INFO |
0x12 |
0x49 |
CUSTOM_DATA |
0x13 |
0x4D |
STA_MAX_CONN_RETRY |
0x14 |
0x51 |
STA_CONN_END_REASON |
0x15 |
0x55 |
STA_CONN_RSSI |
0x16 |
0x59 |
下面的例子都基于当前仓库的 buildPacket() 真实逻辑。
方法:
getBlufiBuildPacketGetNegotitionData()负载固定为:
[0x00, 0x01, 0x07, 0x01]
含义分别是:
0x00: 协商负载里的 frame control0x01: 版本0x07: 安全模式标记0x01: 校验类型
如果当前序列号是 0x00,完整字节如下:
01 00 00 04 00 01 07 01
说明:
01:DATA + NEG00: 外层包头frameCtrl00: 序列号04: 负载长度00 01 07 01: 协商负载
注意:当前页面流程里并没有实际发送这个协商包,且项目也没有真正开启加密通讯,所以移植时可以把它视为“可选逻辑”。
方法:
getBlufiBuildPacketSetOpModeSTA()负载:
01
其中:
0x01表示WIFI_OP_MODE.STA
假设当前序列号是 0x00,完整字节:
08 00 00 01 01
解释:
08:CTRL + SET_WIFI_OPMODE00: 外层frameCtrl00: 序列号01: 负载长度01:STA
方法:
getBlufiBuildPacketSetSSID(ssid)这一步把 SSID 按字节直接放进负载,当前实现是逐字符 charCodeAt,对纯 ASCII 字符串最直接。
例如 SSID 为 TestWiFi:
SSID 字节: 54 65 73 74 57 69 46 69
假设当前序列号是 0x01,完整字节:
09 00 01 08 54 65 73 74 57 69 46 69
解释:
09:DATA + STA_SSID00: 外层frameCtrl01: 序列号08: SSID 长度- 后面 8 个字节就是 SSID 内容
方法:
getBlufiBuildPacketSetPassword(password)例如密码为 12345678,假设当前序列号是 0x02:
0D 00 02 08 31 32 33 34 35 36 37 38
解释:
0D:DATA + STA_PASSWD00: 外层frameCtrl02: 序列号08: 密码长度- 后面是密码 ASCII 字节
方法:
getBlufiBuildPacketSetConnectAP()假设当前序列号是 0x03:
0C 00 03 00
解释:
0C:CTRL + CONN_TO_AP00: 外层frameCtrl03: 序列号00: 无负载
方法:
getBlufiBuildPacketGetWiFiStatus()例如序列号为 0x04:
14 00 04 00
方法:
getBlufiBuildPacketGetVersion()例如序列号为 0x05:
1C 00 05 00
方法:
getBlufiBuildPacketGetScanWiFiList()例如序列号为 0x06:
24 00 06 00
方法:
getBlufiBuildPacketGetCustomData(data)例如发送字符串 hello,其字节为:
68 65 6C 6C 6F
假设当前序列号是 0x07,完整字节:
4D 00 07 05 68 65 6C 6C 6F
pages/device/device.js 中,确认配网时发送顺序如下:
SET_WIFI_OPMODE(STA)STA_SSIDSTA_PASSWDCONN_TO_AP
也就是:
08 ...
09 ...
0D ...
0C ...
如果你在其他平台复刻这个项目,优先按这个顺序发即可。
设备 Notify 上来的数据,项目中仍然是看第 1 个字节来识别:
type = byte0 & 0x03
subtype = byte0 >> 2
因此无论在哪个平台,你的解析入口都建议先做下面两步:
- 取
byte0 - 先拆出
type和subtype - 再根据
type/subtype进入对应分支解析负载
当前项目主要处理这些返回类型:
| type | subtype | 含义 |
|---|---|---|
DATA |
NEG |
协商响应 |
DATA |
WIFI_LIST |
WiFi 列表 |
DATA |
WIFI_REP |
WiFi 状态报告 |
DATA |
REPLY_VERSION |
版本响应 |
DATA |
ERROR_INFO |
错误信息 |
DATA |
STA_CONN_RSSI |
连接 RSSI |
CTRL |
ACK 等 |
控制命令确认 |
这里同样不是完整官方协议,而是当前仓库已经落地处理过的几种响应。
当前项目按下面格式解析:
[major][minor]
例如:
01 00
表示版本 1.0。
当前项目按下面格式解析:
[errorCode]
常见错误码映射:
| 错误码 | 含义 |
|---|---|
0x00 |
成功 |
0x01 |
协商失败 |
0x02 |
校验失败 |
0x03 |
解密失败 |
0x04 |
数据包格式错误 |
0x05 |
不支持的安全模式 |
0x06 |
序列号错误 |
0x07 |
数据长度错误 |
0x08 |
操作失败 |
0x09 |
内存不足 |
0x0A |
参数错误 |
0x0B |
WiFi 连接失败 |
0x0C |
WiFi 断开失败 |
0x0D |
获取 WiFi 列表失败 |
0x0E |
获取 WiFi 状态失败 |
0x0F |
未知错误 |
项目里按“多条记录拼接”的方式解析,每条记录格式为:
[len][rssi][ssidBytes...]
其中:
len是当前记录总长度,不含自己这个长度字节rssi为 1 字节有符号值,代码里用byte - 256转成负数- 后面的字节为 SSID
例如某条记录可能像这样:
09 D8 54 65 73 74 57 69 46
可理解为:
09: 本条记录后续长度为 9D8: RSSI,转成十进制约为-40- 后续字节是 SSID 内容
项目里先取前 3 个字节:
[opMode][staConnStatus][softapConnNum]
后面的内容,再按 TLV 解析:
[type][length][data...]
当前代码已经识别的 TLV 类型主要是:
| TLV type | 含义 |
|---|---|
0x15 |
STA_CONN_END_REASON |
0x16 |
STA_CONN_RSSI |
也就是说,收到 WIFI_REP 后,你至少可以先这样解析:
- 取第 1 字节作为 WiFi 工作模式
- 取第 2 字节作为 STA 连接状态
- 取第 3 字节作为 SoftAP 连接数
- 剩余字节按
TLV(type, len, data)循环读取
项目中用到的工作模式值:
| 值 | 含义 |
|---|---|
0x00 |
NULL |
0x01 |
STA |
0x02 |
SOFTAP |
0x03 |
SOFTAP_STA |
项目中用到的 STA 状态值:
| 值 | 含义 |
|---|---|
0x00 |
已连接成功 |
0x01 |
连接失败 |
0x02 |
连接中 |
0x03 |
已连上但还没有 IP |
handleNotification() 里对设备返回包做了额外处理,逻辑来自 isDataEnd():
- 它读取通知包的第 2 个字节作为“帧控制位图”
- 如果某个位表示带校验,则会裁掉尾部 2 字节
- 如果判断为未分包,则直接去掉前 4 字节头部
- 如果判断为分包,则去掉前 6 字节并等待后续包拼接
这说明当前接收侧默认认为设备返回包可能比发送包多出一些控制信息,尤其是分包时会多 2 字节。
但这里要特别注意:
- 当前仓库对返回包头位定义只有代码经验,没有完整注释。
- 如果你要做 Android、iOS、Web Bluetooth 或其他 BLE 平台版本,建议先按本文的发送格式实现。
- 对接收侧分包、校验、加密位的完整处理,最好再对照一份 ESP-BLUFI 官方定义做最终确认。
如果你要在其他平台复刻,建议按下面的最小实现来做:
function buildPacket(type, subtype, payload, sequence) {
const data = payload || [];
const byte0 = (subtype << 2) | type;
return new Uint8Array([
byte0,
0x00,
sequence & 0xff,
data.length & 0xff,
...data
]);
}- 设置 STA 模式:
buildPacket(0x00, 0x02, [0x01], seq) - 设置 SSID:
buildPacket(0x01, 0x02, ssidBytes, seq) - 设置密码:
buildPacket(0x01, 0x03, passwdBytes, seq) - 连接 AP:
buildPacket(0x00, 0x03, [], seq) - 查询版本:
buildPacket(0x00, 0x07, [], seq) - 查询 WiFi 列表:
buildPacket(0x00, 0x09, [], seq) - 查询 WiFi 状态:
buildPacket(0x00, 0x05, [], seq)
- 取
byte0 - 解出
type和subtype - 根据
subtype解析负载
发送时使用的是:
[typeSubtype][frameCtrl][sequence][length][payload]
但 parsePacket() 里却写成了:
sequence = data[1]
length = data[3]
也就是它把 data[1] 当成了序列号,而发送侧实际把 data[1] 写成了 frameCtrl。
因此你在其他平台实现时,建议自己统一成一套明确规则,不要原样照抄这个小差异。
blufi.js里有NEG协商包构造- 页面上也有“加密通讯”入口
- 但页面实际提示“不支持加密模式通讯”
所以现阶段最靠谱的移植目标,是先把“明文配网流程”完整跑通。
当前 getBlufiBuildPacketSetSSID() / getBlufiBuildPacketSetPassword() 使用的是逐字符 charCodeAt()。
这对英文和数字网络名通常没问题,但如果你后续要兼容中文 SSID,建议跨平台实现时统一改成 UTF-8 编码。
如果只看这个仓库实际能跑通的核心逻辑,可以把它理解为:
- 第 1 字节
Byte0决定这是哪个type/subtype - 后面跟一个固定
0x00的控制字节 - 再跟序列号和负载长度
- 再放具体数据
- 配网时按
STA 模式 -> SSID -> 密码 -> 连接 AP顺序发送
这样你就可以很快在其他平台先复刻出一版可用的 BLUFI 明文配网流程。