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

Posted

技术标签:

【中文标题】如何将 BLE 设备中的十六进制数据分解为可用数据? (速度和节奏)【英文标题】:How to break down hex data into usable data from BLE Device? (Speed and Cadence) 【发布时间】:2021-03-26 16:54:20 【问题描述】:

我正在构建一个简单地显示速度和节奏的 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 是最重要的,因为它包含数据。

我在这里找到了规格。

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

【问题讨论】:

最好依赖:characteristic.value.map String(format: "%02hhx", $0) .joined()。您有 11 个八位字节大小的数据,第一个八位字节用于标志,从 2 到 6,用于车轮旋转等,直到 LastCrank 事件时间。 可能有帮助:***.com/q/38023838/1187415. 【参考方案1】:

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

extension Data 
    // Based on Martin R's work: https://***.com/a/38024025/97337
    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
    

这是创建 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
        
    

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

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的使用;即使您认为自己永远不会在大端平台上运行,也最好在解码时保持精确:

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)))

【讨论】:

【参考方案2】:

首先我必须提到,我对 BLE 没有任何经验,也没有你想要做什么。但是由于还没有答案,我会告诉你我对要求的理解。

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

它有 11 个字节的布局,如下所示:

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

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

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

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

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

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

这意味着接下来的4个字节0x07000000代表旋转数据。我不知道这个数字应该是多少。

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

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

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

【讨论】:

这是一个好的开始,我会研究一下。谢谢!

以上是关于如何将 BLE 设备中的十六进制数据分解为可用数据? (速度和节奏)的主要内容,如果未能解决你的问题,请参考以下文章

如何将 BLE RSSI 显示为信号强度条 iPhone

BlueZ DBUS API - GATT 接口对 BLE 设备不可用

Android 可穿戴设备可以与 iOS 一起使用,通过 BLE 连接吗?有没有可用的SDK?

低功耗蓝牙Ble的详细使用流程

如何将音频 HEX 文件发送到 Ble 设备

如何在Android中为BLE编写快速稳定的特性?