蓝牙问题排查与协议解析笔记

3 阅读9分钟

智迈蓝牙问题排查与协议解析笔记

本文整理本次排查过程中确认过的协议规则、典型问题、数据解析方法,以及调试建议,方便后续继续定位智迈蓝牙控车问题。

1. 目前确认过的现象

1.1 链路通,但控车看起来“没反应”

从日志看,以下链路已经打通:

  1. 扫码成功
  2. 扫描命中设备
  3. 蓝牙连接成功
  4. 发现服务成功
  5. write characteristic 可写
  6. 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 F0D0 是什么

  • F0:手机下发的“上下电控制命令”
    • 0xF0, 0x01:车辆上电
    • 0xF0, 0x00:车辆下电
  • D0:车辆状态字节中的第 0 位,表示“上电状态”
    • 1:车辆上电
    • 0:车辆下电

结合业务语义:

  • 上电 = 解锁 = 屏幕点亮
  • 下电 = 上锁 = 屏幕熄灭

因此:

  • 当前下电时,点击按钮发 F0 01,表示上电/解锁
  • 当前上电时,点击按钮发 F0 00,表示下电/上锁

2.2 data[4] 的 D0~D6 位定义

data[4] 是车辆状态字节,各 bit 的定义如下:

Bit掩码含义
D00x01上电状态
D10x02解防/设防状态
D20x04寻车状态
D30x08坐桶锁状态
D40x10无感解锁状态
D50x20请求绑定状态
D60x40绑定状态

例如:

  • 0x40 = 0100 0000:已绑定,其余状态关闭
  • 0x41 = 0100 0001:已绑定 + 上电
  • 0x45 = 0100 0101:已绑定 + 上电 + 寻车开启

3. 为什么 (vehicleStatus & 0x08) != 0

以坐桶锁 D3 为例:

  • D3 表示第 3 位
  • 第 3 位的掩码就是 1 << 3 = 0x08

所以:

(vehicleStatus & 0x08) != 0

表示:

  1. 0x08 只取出第 3 位
  2. 如果结果不等于 0,说明第 3 位是 1
  3. 如果结果等于 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)

含义分两步:

  1. data[4] >> 2
    • 把整个二进制右移 2 位
    • 原来的 D2 被移动到最低位
  2. & 0x01
    • 只保留最低位
    • 最终得到 01

所以:

  • ((x >> n) & 0x01):取第 n 位的值,结果是整数 0/1
  • (x & mask) != 0:判断某一位是否为 1,结果是布尔值 true/false

5. 为什么 speedmileage 要左移

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 新规则

新的智迈校验规则是:

  1. 先对原帧所有字节做 XOR
  2. 再把以下 8 个固定字节继续 XOR
  3. 最终结果才是校验字节

固定异或字节:

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: 数据不合法

优先检查:

  1. 包头是否正确
  2. 校验规则是否匹配当前协议版本
  3. 收到的是不是完整一帧
  4. 当前设备是不是 Fly-OTA-*

10. 排查建议

10.1 优先确认的 4 件事

  1. 连到的是不是正常运行态设备,而不是 Fly-OTA-*
  2. 发送完成后,回包有没有变化
  3. 回包里的 D0/D1/D2 状态位有没有变化
  4. UI 展示是不是按照正确状态位渲染

10.2 建议保留的调试信息

远程排查时,建议至少保留这些信息:

  • 目标 MAC
  • 实际连接设备名
  • 最近发送报文
  • 最近接收报文
  • 连接状态时间线
  • 是否解析成功
  • power/alarm/search/... 状态摘要

10.3 现阶段结论

就目前排查结论看,控车“没反应”不能简单归因于一个点,主要有三类可能:

  1. 写入并发导致系统拒绝写入
  2. 连接到了 Fly-OTA-* 这类异常设备态
  3. 校验规则变更后,旧解析逻辑把合法包误判为非法

其中第 3 点已经通过新校验规则得到修正。