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

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

我在这里找到了规范。

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

最佳答案

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

extension Data {
    // Based on Martin R's work: https://stackoverflow.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
        }
    }
}

标志是一个 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 的使用;即使您认为自己永远不会在大端平台上运行,解码时也要精确:

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

https://stackoverflow.com/questions/66821181/

相关文章:

c# - 添加新包会破坏 .NET 5 应用程序

ios - SwiftUI:如何知道修饰符的正确顺序?

rust - 如何安全地初始化 `NonZeroU8` 类型的常量?

go - Go 支持函数式编程吗?

firebase - 在 Flutter 中使用相同的 Firebase 将同一应用程序中的用户和卖

hibernate - 如何使用 Quarkus 和 Panache 在 snake case 中获

c++ - 为什么不打开 char 值达到 case 0xC2?

c - 这个 C 函数是什么意思?函数指针?

qt - 如何将 QML 文件组织到嵌套文件夹中?

java - 如何使用 Jackson 从 Json 文件中读取特定对象