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

Yot*_*jon 2 ios core-bluetooth bluetooth-lowenergy swift

我正在构建一个仅显示速度和节奏的 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>
Run Code Online (Sandbox Code Playgroud)

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

<CBCharacteristic: 0x282de4420, UUID = 2A5B, properties = 0x10, value = {length = 11, bytes = 0x0307000000442c0500af25}, notifying = YES>
Run Code Online (Sandbox Code Playgroud)

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

我在这里找到了规格。 在此输入图像描述

我只是看看这个十六进制数据和这个规格表,感觉好像我在看胡言乱语。该规格表与数据有什么关系?十六进制数据的每个部分是否被分配了一个特定值,或者整个十六进制是一个奇异值?我从哪说起呢?感谢您的帮助!

Rob*_*ier 5

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

extension Data {
    // Based on Martin R's work: /sf/answers/2661681781/
    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: $0)} )
        removeFirst(valueSize)
        return value
    }
}
Run Code Online (Sandbox Code Playgroud)

这是主解码器,它创建一个 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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

标志是一个选项集,并以这种方式解码:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

RevolutionData 就是这样解码的。注意使用.littleEndian; 即使您认为自己永远不会在大端平台上运行,解码时保持精确也是有好处的:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

注意使用Int(clamping:). UInt32这对于您的特定用途来说并不是严格需要的,但在 32 位平台上使用(或更大的)调用此代码是合法的。这可能会溢出并崩溃。决定在这种情况下做什么是一个重要的选择,但init(clamping:)如果坏数据不会造成灾难性的后果并且您不想崩溃,那么 a 是一个很好的默认值。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)))
Run Code Online (Sandbox Code Playgroud)