数据解析与序列化:字节序处理、自定义协议设计、Protobuf/FlatBuffers应用、CRC校验

做 BLE 开发,说白了就是在跟字节打交道。你发出去的是字节数组,收进来的也是字节数组。怎么把这些原始字节变成有意义的业务数据?这就是我们今天要聊的核心问题。

我刚开始做 BLE 项目时,就踩过一个坑。当时对接一个心率设备,文档里写的是小端序,我习惯性地按大端序解析了。结果心率值显示 180,实际只有 45。嗯,从那以后,字节序这件事我再也不敢马虎了。

字节序处理:大端与小端的抉择

字节序,也叫端序。说白了就是多字节数据在内存里怎么排列。大端序(Big-Endian)把高位字节放在低地址,小端序(Little-Endian)则相反。

在 BLE 协议里,大部分数据使用小端序。比如蓝牙核心规范里定义的 GATT 特征值,几乎都是小端。但有个例外——UUID 和某些厂商自定义数据可能用大端。你想想看,如果不搞清楚这个,解析出来的数据全是错的。

核心原则: 拿到 BLE 数据后,先看文档确认字节序。没有文档?那就抓包对比已知值,反向推导。

在 Kotlin 里处理字节序,我一般用 java.nio.ByteBuffer。它支持设置字节序,用起来很顺手。

fun parseHeartRate(data: ByteArray): Int {
    val buffer = ByteBuffer.wrap(data)
    buffer.order(ByteOrder.LITTLE_ENDIAN) // BLE 通常用小端
    return buffer.getShort().toInt() // 读取 2 字节心率值
}

这里有个细节要注意:ByteBuffer.wrap() 默认是大端序。如果你不显式设置 order(),解析结果就错了。我曾经在代码 review 时发现同事漏了这行,帮他省了一次线上事故。

小技巧: 在项目里定义一个全局的 ByteOrder 常量,比如 val BLE_BYTE_ORDER = ByteOrder.LITTLE_ENDIAN,所有解析统一引用。这样改起来也方便。

自定义协议设计:从零搭建通信规约

很多时候,标准 BLE 服务满足不了需求。比如你要传输自定义的传感器数据、控制指令,就得自己设计协议。我参与过一个工业项目,设备每 100ms 上报 20 多个参数,用标准特征值根本塞不下。

设计自定义协议,我总结了几个要点:

  • 固定帧头:用 2 字节魔数(比如 0xAA 0x55)标识帧起始,接收方据此同步。
  • 长度字段:1-2 字节表示 payload 长度,方便分包和校验。
  • 命令字:1 字节区分不同功能,比如 0x01 表示温度数据,0x02 表示控制指令。
  • 序列号:可选,用于去重和应答匹配。
  • 校验:CRC 或累加和,保证数据完整性。
  • 帧尾:可选,但建议加,比如 0x0D 0x0A

举个例子,一个简单的温湿度协议帧:

// 帧结构:帧头(2) + 长度(1) + 命令(1) + 数据(N) + CRC(2)
// 温度数据:命令 0x01,数据 4 字节(float)
// 湿度数据:命令 0x02,数据 4 字节(float)

fun encodeTempHumidity(temp: Float, humidity: Float): ByteArray {
    val payload = ByteArray(8)
    val buffer = ByteBuffer.wrap(payload).apply {
        order(ByteOrder.LITTLE_ENDIAN)
        putFloat(temp)
        putFloat(humidity)
    }
    
    val frame = ByteArray(2 + 1 + 1 + 8 + 2)
    var idx = 0
    frame[idx++] = 0xAA.toByte()  // 帧头
    frame[idx++] = 0x55.toByte()
    frame[idx++] = 8               // 长度
    frame[idx++] = 0x01            // 命令(温湿度合并上报)
    payload.copyInto(frame, idx)
    idx += 8
    
    val crc = calculateCRC16(frame, 0, idx) // 计算 CRC
    frame[idx++] = (crc and 0xFF).toByte()
    frame[idx] = (crc shr 8).toByte()
    return frame
}
注意: 自定义协议一定要考虑兼容性。我见过一个项目,协议版本升级后,旧设备和新 App 无法通信,就是因为没设计版本字段。建议在帧头或命令字里预留 1 字节表示协议版本。

Protobuf 与 FlatBuffers:结构化数据的序列化方案

当数据字段越来越多,手动拼字节就变得很痛苦。这时候就该上序列化工具了。BLE 开发里常用的有两种:Protobuf 和 FlatBuffers。

Protobuf 是 Google 的序列化库,压缩率高,适合带宽有限的 BLE 场景。但它的解析需要拷贝整个数据包,延迟稍高。

FlatBuffers 也是 Google 的,但设计思路不同。它支持零拷贝解析,你直接访问底层字节,不需要反序列化。这对实时性要求高的场景很友好。

我个人习惯:如果数据量小(几十字节以内),用 Protobuf 就够了。如果数据量大或者需要频繁解析,比如每秒几十次上报,我会选 FlatBuffers。

来看一个 Protobuf 的例子。假设我们定义了一个传感器数据消息:

// sensor.proto
message SensorData {
    uint32 timestamp = 1;
    float temperature = 2;
    float humidity = 3;
    uint32 battery = 4;
}

在 Kotlin 里使用:

// 编码
val data = SensorData.newBuilder()
    .setTimestamp(System.currentTimeMillis() / 1000)
    .setTemperature(25.3f)
    .setHumidity(60.5f)
    .setBattery(85)
    .build()
val bytes = data.toByteArray() // 直接发送这个字节数组

// 解码
val parsed = SensorData.parseFrom(receivedBytes)
Log.d("BLE", "温度: ${parsed.temperature}")

FlatBuffers 的用法稍微复杂一点,但性能更好。我参与过一个运动手环项目,加速度数据每秒 100 组,每组 6 个 float。用 Protobuf 解析一次要 2ms,换成 FlatBuffers 后降到 0.1ms。差距很明显。

选型建议: 如果团队熟悉 Protobuf,且数据量不大,优先用 Protobuf。如果追求极致性能,或者数据包超过 256 字节,考虑 FlatBuffers。另外,FlatBuffers 的 schema 变更后,旧数据仍然可读,这点比 Protobuf 灵活。

CRC 校验:确保数据在传输中没被篡改

BLE 的空中传输其实有 CRC,但那是在链路层。应用层的数据仍然可能出错——比如手机和设备的缓冲区不同步,或者中间有干扰。所以应用层再加一道 CRC 是很有必要的。

最常用的是 CRC-16,生成多项式是 0x8005。Kotlin 实现起来也不复杂:

fun calculateCRC16(data: ByteArray, offset: Int, length: Int): Int {
    var crc = 0xFFFF
    for (i in offset until offset + length) {
        crc = crc xor (data[i].toInt() and 0xFF)
        for (j in 0 until 8) {
            crc = if ((crc and 0x0001) != 0) {
                (crc shr 1) xor 0xA001
            } else {
                crc shr 1
            }
        }
    }
    return crc.inv() and 0xFFFF // 取反后返回
}

这段代码用的是 CRC-16/MODBUS 算法,很多 BLE 设备都用这个。注意最后要取反,不然和设备的校验值对不上。我曾经在这个细节上折腾了半天,最后发现是取反的问题。

校验时,接收方对帧头到数据部分重新计算 CRC,然后和帧尾的 CRC 比较。如果一致,说明数据完整。不一致,直接丢弃,请求重发。

经验之谈: CRC 计算要包含整个帧头和数据,但不包含 CRC 本身。另外,有些设备会把 CRC 放在数据前面,有些放在后面。一定要看文档确认。

知识体系总览

下面这张图总结了本章的核心逻辑。从原始字节到业务数据,每一步都有对应的技术方案。

BLE 数据解析与序列化知识体系 原始字节数组 字节序处理(大端/小端) 自定义协议 帧头+长度+命令+数据+CRC Protobuf 高压缩率,需反序列化 FlatBuffers 零拷贝解析,高性能 CRC-16 校验(完整性验证) 最终输出:结构清晰、完整可靠的业务数据

从图中可以看到,原始字节数组经过字节序处理,进入三种不同的序列化方案。无论选哪种,最后都要过 CRC 校验这一关。这三步走完,数据才算真正可用。

好了,关于数据解析与序列化,核心内容就是这些。字节序别搞反,协议设计留好扩展,序列化工具按需选,CRC 校验别省略。做到这四点,你的 BLE 数据通信基本就稳了。

最后提醒: 实际项目中,建议把解析逻辑封装成独立的模块,方便单元测试。我习惯写一个 BleDataParser 类,所有协议解析都放在里面,这样调试和修改都方便。

公众号:蓝海资料掘金营,微信deep3321