2
votes

I'm building an iOS app that simply displays speed and cadence. I was successfully connected to my BLE device and received data. I simply don't know what to do from here. How do I make sense of this data?

Here is the received Data

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>

As far as I understand each time I'm getting notified, it represents the most updated data from the BLE device. I'm assuming in the repeating line that has a UUID of 2A5B represents the raw data represented in "bytes".

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

I'm also assuming that this hex data 0x0307000000442c0500af25 is most significant as it contains data.

I've found specifications here. enter image description here

I simply look at this hex data and this specification sheet and feel as if I'm looking at gibberish. What does this specification sheet have to do with the data? Does each part of the hex data get assigned a specific value or is the whole hex a singular value? Where do I start? Thank you for your help!

2
Better rely on: characteristic.value.map { String(format: "%02hhx", $0) }.joined(). You have a 11 octets size data, first octet is for the flag, from 2 to 6, it's for the wheel revolution, etc. until LastCrank Event Time.Larme

2 Answers

4
votes

First, don't think about this as "hex data." This is just a sequence of bytes. It happens to be displayed in hex, just because that's often useful. But the data coming from the device is not "in hex." It's just a bunch of bytes, and you need to decode those bytes as the spec indicates. The best way to decode bytes, IMO, is by consuming them as you go along. Subscripting Data is dangerous because the first index is not promised to be 0. I use the following to do that:

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

Here's the main decoder, which creates a CSCData struct (this could be a bit nicer with throws, but it add complexity to the example):

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

Flags are an OptionSet and are decoded this way:

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

And RevolutionData is decoded this way. Note the use of .littleEndian; it's good to be precise when decoding even if you think you'll never run on a big endian platform:

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

Note the use of Int(clamping:). This isn't stricly needed for your specific use, but it's legal to call this code with a UInt32 (or larger) on a 32-bit platform. That could overflow and crash. Deciding what to do in that case is an important choice, but a init(clamping:) is a good default if bad data would not be catastrophic and you don't want to crash. This isn't needed for TimeInterval, since it's certain to be larger than UInt16.

The deeper point about this is that when decoding data you get from Bluetooth, you should always be very defensive. You may have misunderstood the spec, or the device may have bugs. They can send you unexpected data, and you should be able to recover from that.

And testing this:

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

Firstly I have to mention that I don't have any experience with BLE and what you're trying to do specifically. But since there is no answer yet I'll tell you what I understand from the requirements.

So you have the hex result coming from your device looking something like this: 0x0307000000442c0500af25

It has 11 bytes layout like this:

  • first byte is the flags byte;
  • next 4 bytes are the wheel revolution data;
  • next 2 last wheel event time;
  • next 2 cumulative crank revolutions;
  • the last 2 are the last crank event time.

Now each of these values has meaning depending on the flag value. So if the first bit from the Flags byte is 0 it means there is no wheel revolution data present and if it is 1 it means that there is. So for the first flag bit being 0 the 4 bytes that represent wheel revolution data contain no relevant data.

If the second bit from the Flags byte is 0 there is no crank revolution data so we don't care about the 2 bytes that represent such data.

The last six bits from the Flags byte are reserved for future use. So we don't care about them at all.

With that of the way you need to analyse the hex data that you receive and perform bitwise operations on them to get the data you need.

In the example of 0x0307000000442c0500af25 the first bit is 0x03. In binary that is 00000011. We count the bits from right to left, so in this case both wheel and crank data are present.

That means that the next 4 bytes which are 0x07000000 represent the revolution data. I don't know what this number is supposed to be.

The next 2 bytes which are 0x442c represent the last wheel event time. And so on until the end.

So thats the main gist of what your problem is. In older to solve it you must learn how to use bitwise operations in Swift, so that you can extract the parts that you need from the data.

You can start with the documentation https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html.