智迈蓝牙问题排查与协议解析笔记
本文整理本次排查过程中确认过的协议规则、典型问题、数据解析方法,以及调试建议,方便后续继续定位智迈蓝牙控车问题。
1. 目前确认过的现象
1.1 链路通,但控车看起来“没反应”
从日志看,以下链路已经打通:
- 扫码成功
- 扫描命中设备
- 蓝牙连接成功
- 发现服务成功
- write characteristic 可写
- notify characteristic 能收到回包
这说明“按钮点击完全没触发”不是主因。
如果用户反馈“点了没反应”,常见原因更可能是:
- 命令发出去了,但车机没有执行
- 命令发出去了,车机执行了,但 UI 状态映射错了
- 收到的不是这次控车的响应包,而是普通状态上报包
- 连到的是
Fly-OTA-*设备,设备并非正常控车态
1.2 ERROR_GATT_WRITE_REQUEST_BUSY
报错:
PlatformException(writeCharacteristic, gatt.writeCharacteristic() returned 201 : ERROR_GATT_WRITE_REQUEST_BUSY, null, null)
含义:
- Android GATT 上一个写请求还没处理完
- App 又发起了新的写请求
- 系统拒绝了后续写入
处理方式:
- 控车写入必须串行化
- 小包不要使用
allowLongWrite: true - 连续点击按钮时要做节流或排队
1.3 connect Timed out after 20s
报错:
FlutterBluePlusException | connect | Timed out after 20s
含义:
- App 已经找到设备
- 也已经调用了
device.connect(...) - 但 20 秒内没有建立成功的 GATT 连接
常见原因:
- 车辆处于可广播但不可连接状态
- 车机休眠,尚未真正唤醒
- 设备被其他手机或系统蓝牙占用
- Android 残留旧 GATT 连接
- 连到的是异常模式设备
2. 智迈协议关键字段
2.1 F0 和 D0 是什么
F0:手机下发的“上下电控制命令”0xF0, 0x01:车辆上电0xF0, 0x00:车辆下电
D0:车辆状态字节中的第 0 位,表示“上电状态”1:车辆上电0:车辆下电
结合业务语义:
- 上电 = 解锁 = 屏幕点亮
- 下电 = 上锁 = 屏幕熄灭
因此:
- 当前下电时,点击按钮发
F0 01,表示上电/解锁 - 当前上电时,点击按钮发
F0 00,表示下电/上锁
2.2 data[4] 的 D0~D6 位定义
data[4] 是车辆状态字节,各 bit 的定义如下:
| Bit | 掩码 | 含义 |
|---|---|---|
| D0 | 0x01 | 上电状态 |
| D1 | 0x02 | 解防/设防状态 |
| D2 | 0x04 | 寻车状态 |
| D3 | 0x08 | 坐桶锁状态 |
| D4 | 0x10 | 无感解锁状态 |
| D5 | 0x20 | 请求绑定状态 |
| D6 | 0x40 | 绑定状态 |
例如:
0x40 = 0100 0000:已绑定,其余状态关闭0x41 = 0100 0001:已绑定 + 上电0x45 = 0100 0101:已绑定 + 上电 + 寻车开启
3. 为什么 (vehicleStatus & 0x08) != 0
以坐桶锁 D3 为例:
D3表示第 3 位- 第 3 位的掩码就是
1 << 3 = 0x08
所以:
(vehicleStatus & 0x08) != 0
表示:
- 用
0x08只取出第 3 位 - 如果结果不等于
0,说明第 3 位是1 - 如果结果等于
0,说明第 3 位是0
同理:
(vehicleStatus & 0x01) != 0:取 D0(vehicleStatus & 0x02) != 0:取 D1(vehicleStatus & 0x04) != 0:取 D2
4. 为什么 ((data[4] >> 2) & 0x01) 能取到 D2
这和上面的按掩码写法是同一件事,只是写法不同。
例子:假设
data[4] = 0x45 = 0100 0101
D2 当前值是 1。
表达式:
((data[4] >> 2) & 0x01)
含义分两步:
data[4] >> 2- 把整个二进制右移 2 位
- 原来的
D2被移动到最低位
& 0x01- 只保留最低位
- 最终得到
0或1
所以:
((x >> n) & 0x01):取第n位的值,结果是整数0/1(x & mask) != 0:判断某一位是否为1,结果是布尔值true/false
5. 为什么 speed、mileage 要左移
5.1 左移的本质
左移表示把二进制整体往高位挪动,右边补 0。
- 左移 1 位,相当于乘 2
- 左移 8 位,相当于乘 256
因为 1 个字节 = 8 位,所以拼多个字节时,每往前一个字节,就要左移 8 位。
5.2 speed = (data[8] << 8) + data[9]
这里速度用 2 个字节表示:
data[8]:高字节data[9]:低字节
如果:
data[8] = 0x12
data[9] = 0x34
那么目标值应当是:
0x1234
做法:
0x12 << 8 = 0x1200
0x34 = 0x0034
相加 = 0x1234
所以左移 8 位的作用是:把高字节放到前面的 8 位上。
5.3 totalMileage = ((data[10] << 16) + (data[11] << 8) + data[12]) / 10.0
这里总里程用 3 个字节表示:
data[10]:高字节data[11]:中字节data[12]:低字节
如果:
data[10] = 0x01
data[11] = 0x23
data[12] = 0x45
拼接结果是:
0x012345
计算过程:
data[10] << 16 = 0x010000
data[11] << 8 = 0x002300
data[12] = 0x000045
相加 = 0x012345
这里为什么是 16?
- 因为最高字节前面要空出 2 个字节的位置
2 * 8 = 16
5.4 / 10.0 是什么
/ 10.0 不是拼字节操作,它是协议精度换算。
例如协议定义:
- 总里程精度
0.1km - 电压精度
0.1V
那么设备上传整数值后,需要再除以 10.0 才得到最终真实值。
6. 为什么 vehicleStatus = data[4] 不是 data[3]
这里要区分“数组下标”和“协议里的第几个字节”。
在 Dart 里:
data[0]表示第 1 个字节data[1]表示第 2 个字节data[4]表示第 5 个字节
智迈状态包头部可按当前协议理解为:
| 下标 | 含义 |
|---|---|
data[0] | 0xAA |
data[1] | 0x55 |
data[2] | 协议版本 |
data[3] | 功能码 |
data[4] | 车辆状态字节 |
所以:
data[3]是功能码data[4]才是车辆状态字节
6.1 padLeft(2, '0') 有什么用
在时间格式化代码里,经常会看到:
time.hour.toString().padLeft(2, '0')
它的作用是:如果字符串长度不足 2,就在左边补 0,直到长度变成 2。
例如:
3->'03'9->'09'12->'12'
所以这类写法:
final hour = time.hour.toString().padLeft(2, '0');
final minute = time.minute.toString().padLeft(2, '0');
final second = time.second.toString().padLeft(2, '0');
最终会把时间统一格式化为:
08:03:05
而不是:
8:3:5
这在日志展示、时间指令拼接、调试输出时更直观,也更规范。
7. 校验规则变更
7.1 旧规则
旧规则是:帧内原始字节逐个 XOR,最后得到 1 个校验字节。
7.2 新规则
新的智迈校验规则是:
- 先对原帧所有字节做 XOR
- 再把以下 8 个固定字节继续 XOR
- 最终结果才是校验字节
固定异或字节:
E3 23 39 67 52 89 34 78
7.3 这 8 个字节整体的 XOR 结果
这 8 个字节整体 XOR 结果是:
0x09
也就是说,新规则等价于:
新校验值 = 旧校验值 ^ 0x09
7.4 为什么之前那包 ... B5 现在合法
之前有一包回包:
AA 55 01 02 00 00 00 01 00 00 00 00 09 03 19 4E 00 0A D8 DF 01 00 00 00 00 10 B5
按旧规则计算,得到的是:
0xBC
但新规则需要继续再异或 0x09:
0xBC ^ 0x09 = 0xB5
因此,这包在新规则下是合法包。
8. Fly-OTA-* 设备的风险
在一次排查日志中,连接到的设备名是:
Fly-OTA-00000001DFD8
这个设备名非常可疑,可能表示:
- OTA 模式
- Bootloader 模式
- 非正常控车运行态
如果连到这类设备,可能出现:
- 可以连接
- 可以写 characteristic
- 也能收到 notify
- 但控车命令不会真正生效
- 回包格式可能与正常状态包不同
所以排查时要重点区分:
Fly-*:正常控车态Fly-OTA-*:可能是升级态或异常态
9. 常见日志应如何解读
9.1 “发送完成”
例如:
[18:01:03] 发送完成[启动寻车]
这只能说明:
- 手机已经成功把数据写给 BLE 特征
不能直接说明:
- 车辆已经执行了命令
9.2 “收到回包”
例如:
[18:01:06] 收到回包
0xAA, 0x55, ...
这只能说明:
- notify 通道收到了某一包数据
不能直接说明:
- 这就是当前按钮对应命令的响应包
9.3 “状态解析失败”
如果报:
Exception: 数据不合法
优先检查:
- 包头是否正确
- 校验规则是否匹配当前协议版本
- 收到的是不是完整一帧
- 当前设备是不是
Fly-OTA-*
10. 排查建议
10.1 优先确认的 4 件事
- 连到的是不是正常运行态设备,而不是
Fly-OTA-* - 发送完成后,回包有没有变化
- 回包里的
D0/D1/D2状态位有没有变化 - UI 展示是不是按照正确状态位渲染
10.2 建议保留的调试信息
远程排查时,建议至少保留这些信息:
- 目标 MAC
- 实际连接设备名
- 最近发送报文
- 最近接收报文
- 连接状态时间线
- 是否解析成功
power/alarm/search/...状态摘要
10.3 现阶段结论
就目前排查结论看,控车“没反应”不能简单归因于一个点,主要有三类可能:
- 写入并发导致系统拒绝写入
- 连接到了
Fly-OTA-*这类异常设备态 - 校验规则变更后,旧解析逻辑把合法包误判为非法
其中第 3 点已经通过新校验规则得到修正。