如何将十六进制数据分解为来自 BLE 设备的可用数据? (速度和节奏)

How to break down hex data into usable data from BLE Device? (Speed and Cadence)

我正在构建一个 iOS 应用程序,它只显示速度和节奏。我已成功连接到我的 BLE 设备并收到数据。我根本不知道从这里做什么。我如何理解这些数据?

这是接收到的数据

central.state is .poweredOn
<CBPeripheral: 0x2838f48c0, identifier = A7DBA197-EF45-A8E5-17FB-DF8505493179, name = DuoTrap S, state = disconnected>
Peripheral(id: 0, name: "DuoTrap S", rssi: -70)
Connected!
<CBService: 0x281cbd380, isPrimary = YES, UUID = Cycling Speed and Cadence>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x03030000005d1601008212}, notifying = NO>
2A5B: properties contains .notify
<CBCharacteristic: 0x282df8660, UUID = 2A5C, properties = 0x2, value = {length = 2, bytes = 0x0700}, notifying = NO>
2A5C: properties contains .read
<CBCharacteristic: 0x282df8420, UUID = 2A5D, properties = 0x2, value = {length = 1, bytes = 0x04}, notifying = NO>
2A5D: properties contains .read
<CBCharacteristic: 0x282df8660, UUID = 2A5C, properties = 0x2, value = {length = 2, bytes = 0x0700}, notifying = NO>
Unhandled Characteristic UUID: 2A5D
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x0307000000442c0500af25}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x0307000000442c0500af25}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x0308000000304506002e43}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x0308000000304506002e43}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x0309000000664c07006a4b}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x030a000000cf500800f14f}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x030b0000005a540900a953}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x030c00000075570b00b459}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x030e0000000f5d0c00815c}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x030f000000a25f0d00265f}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x030f000000a25f0d00265f}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x030f000000a25f0d00265f}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x030f000000a25f0d00265f}, notifying = YES>
<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x030f000000a25f0d00265f}, notifying = YES>

据我了解,每次收到通知时,它代表来自 BLE 设备的最新数据。我假设在 UUID 为 2A5B 的重复行中代表以“字节”表示的原始数据。

<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x0307000000442c0500af25}, notifying = YES>

我还假设此十六进制数据 0x0307000000442c0500af25 最重要,因为它包含数据。

我在这里找到了规范。

我只是看看这个十六进制数据和这个规范sheet,感觉好像我在看乱码。这个规范 sheet 与数据有什么关系? 是否为十六进制数据的每一部分分配了特定值,或者整个十六进制是否为奇异值?我从哪里开始?感谢您的帮助!

首先,我必须指出,我没有任何 BLE 经验,也没有您具体要做什么。不过目前还没有答案,我就说说我从需求中了解到的情况吧。

所以您的设备的十六进制结果看起来像这样:0x0307000000442c0500af25

它有这样的 11 字节布局:

  • 第一个字节是标志字节;
  • 接下来的4个字节是车轮转数数据;
  • 下2个最后轮事件时间;
  • 接下来的 2 个累计曲柄转数;
  • 最后2个是上次曲柄事件时间。

现在,根据标志值,这些值中的每一个都有意义。 因此,如果 Flags 字节的第一位为 0,则表示不存在车轮转数数据,如果为 1,则表示存在。因此,对于第一个标志位为 0 的 4 个字节表示车轮旋转数据不包含相关数据。

如果 Flags 字节的第二位为 0,则没有曲柄转数数据,因此我们不关心代表此类数据的 2 个字节。

标志字节的最后六位保留供将来使用。所以我们根本不关心他们。

通过这种方式,您需要分析接收到的十六进制数据并对它们执行按位运算以获得所需的数据。

在 0x0307000000442c0500af25 的示例中,第一位是 0x03。在二进制中是 00000011。我们从右到左计算位数,因此在这种情况下,车轮和曲柄数据都存在。

也就是说接下来的4个字节0x07000000代表转数数据。我不知道这个数字应该是多少。

接下来的2个字节0x442c表示最后一次车轮事件时间。以此类推直到结束。

这就是您的问题的主要要点。在旧的解决它你必须学习如何使用Swift中的位运算,这样你就可以从数据中提取你需要的部分。

您可以从文档开始 https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html

首先,不要将其视为“十六进制数据”。这只是一个字节序列。它恰好以十六进制显示,只是因为这通常很有用。但是来自设备的数据不是“十六进制的”。它只是一堆字节,您需要按照规范指示对这些字节进行解码。解码字节的最佳方法,IMO,是在你进行的过程中使用它们。订阅数据是危险的,因为第一个索引未承诺为 0。我使用以下方法来做到这一点:

extension Data {
    // Based on Martin R's work: 
    mutating func consume<T>(type: T.Type) -> T? where T: ExpressibleByIntegerLiteral {
        let valueSize = MemoryLayout<T>.size
        guard count >= valueSize else { return nil }
        var value: T = 0
        _ = Swift.withUnsafeMutableBytes(of: &value, { copyBytes(to: [=10=])} )
        removeFirst(valueSize)
        return value
    }
}

这是创建 CSCData 结构的主要解码器(使用 throws 可能会更好一些,但它增加了示例的复杂性):

struct CSCData {
    var wheelRevolutions: RevolutionData?
    var crankRevolutions: RevolutionData?

    init?(data: Data) {

        var data = data // Make mutable so we can consume it

        // First pull off the flags
        guard let flags = Flags(consuming: &data) else { return nil }

        // If wheel revolution is present, decode it
        if flags.contains(.wheelRevolutionPresent) {
            guard let value = RevolutionData(consuming: &data, countType: UInt32.self) else {
                return nil
            }
            self.wheelRevolutions = value
        }

        // If crank revolution is present, decode it
        if flags.contains(.wheelRevolutionPresent) {
            guard let value = RevolutionData(consuming: &data, countType: UInt16.self) else {
                return nil
            }
            self.crankRevolutions = value
        }

        // You may or may not want this. Left-over data suggests that there was an error
        if !data.isEmpty {
            return nil
        }
    }
}

标志是一个 OptionSet 并以这种方式解码:

struct Flags : OptionSet {
    let rawValue: UInt8

    static let wheelRevolutionPresent = Flags(rawValue: 1 << 0)
    static let crankRevolutionPresent = Flags(rawValue: 1 << 1)
}

extension Flags {
    init?(consuming data: inout Data) {
        guard let byte = data.consume(type: UInt8.self) else { return nil }
        self.init(rawValue: byte)
    }
}

RevolutionData就是这样解码的。注意 .littleEndian 的使用;即使你认为你永远不会 运行 在 big endian 平台上,解码时也要精确:

struct RevolutionData {
    var revolutions: Int
    var eventTime: TimeInterval

    init?<RevolutionCount>(consuming data: inout Data, countType: RevolutionCount.Type)
    where RevolutionCount: FixedWidthInteger
    {
        guard let count = data.consume(type: RevolutionCount.self)?.littleEndian,
              let time = data.consume(type: UInt16.self)?.littleEndian
        else {
            return nil
        }

        self.revolutions = Int(clamping: count)
        self.eventTime = TimeInterval(time) / 1024.0    // Unit is 1/1024 second
    }
}

注意 Int(clamping:) 的用法。这不是您的特定用途所必需的,但在 32 位平台上使用 UInt32(或更大)调用此代码是合法的。那可能会溢出并崩溃。决定在这种情况下做什么是一个重要的选择,但如果错误数据不会造成灾难性后果并且您不想崩溃,则 init(clamping:) 是一个很好的默认值。 TimeInterval 不需要这个,因为它肯定比 UInt16 大。

更深层次的一点是,在解码从蓝牙获得的数据时,您应该始终保持高度防御。您可能误解了规范,或者设备可能存在错误。他们可能会向您发送意外数据,您应该能够从中恢复。

并测试这个:

let data = Data([0x03,0x07,0x00,0x00,0x00,0x44,0x2c,0x05,0x00,0xaf,0x25])
let result = CSCData(data: data)!
// CSCData(wheelRevolutions: Optional(RevolutionData(revolutions: 7, eventTime: 11.06640625)), 
//         crankRevolutions: Optional(RevolutionData(revolutions: 5, eventTime: 9.4208984375)))