How to read a BLE Characteristic Float in Swift

2019-05-03 20:36发布

问题:

I am trying to connect to a Bluetooth LE / Bluetooth Smart / BLE health device's Health Thermometer Service (0x1809), as officially described here: https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.health_thermometer.xml. Specifically, I'm requesting notifications from the Health Thermometer Characteristic (0x2A1C), with description here: https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.temperature_measurement.xml.

I have a decent Swift 2 background, but I've never worked this closely with NSData, bytes, or bitwise operators and I'm completely new to Little Endian vs. Big Endian, so this is pretty new for me and I could use some help. The characteristic has some logic built in that determines what data you will receive. I have received data in the order of Flags, Temperature Measurement Value and Time Stamp 100% of the time so far, but unfortunately I'm always going to get control logic of "010" which means I'm reading the flags incorrectly. In truth, I think I'm bringing in everything shy of the timestamp incorrectly. I'm including what data I'm seeing in the code comments.

I've tried multiple ways of obtaining this binary data. The flags are a single byte with bit operators. The temperature measurement itself is a Float, which it took me some time to realize that it's not a Swift Float, but rather a ISO/IEEE Standard "IEEE-11073 32-bit FLOAT" with what the BLE spec says has "NO EXPONENT VALUE" here: https://www.bluetooth.com/specifications/assigned-numbers/format-types. I don't even know what that means. Here is my code from the didUpdateValueForCharacteristic() function where you can view my multiple attempts that I commented out as I tried a new one:

// Parse Characteristic Response
let stream = NSInputStream( data: characteristic.value! )
stream.open()    // IMPORTANT

// Retrieve Flags
var readBuffer = Array<UInt8>( count: 1, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )
var flags = String( readBuffer[ 0 ], radix: 2 )
flags = String( count: 8 - flags.characters.count, repeatedValue: Character( "0" ) ) + flags
flags = String( flags.characters.reverse() )
print( "FLAGS: \( flags )" )

// Example data:
// ["01000000"]
//
// This appears to be wrong.  I should be getting "10000000" according to spec

// Bluetooth FLOAT-TYPE is defined in ISO/IEEE Std. 11073
// FLOATs are 32 bit
// Format [8bit exponent][24bit mantissa]

/* Attempt 1 - Read in a Float - Doesn't work since it's an IEEE Float
readBuffer = Array<UInt8>( count: 4, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )
var tempData = UnsafePointer<Float>( readBuffer ).memory

// Attempt 2 - Inverted bytes- Doesn't work since it's wrong and it's an IEEE Float
let readBuffer2 = [ readBuffer[ 3 ], readBuffer[ 2 ], readBuffer[ 1 ], readBuffer[ 0 ] ]
var tempValue = UnsafePointer<Float>( readBuffer2 ).memory
print( "TEMP: \( tempValue )" )

// Attempt 3 - Doesn't work for 1 or 2 since it's an IEEE Float
var f:Float = 0.0
memccpy(&f, readBuffer, 4, 4)
print( "TEMP: \( f )" )
var f2:Float = 0.0
memccpy(&f2, readBuffer2, 4, 4)
print( "TEMP: \( f2 )" )

// Attempt 4 - Trying to Read an Exponent and a Mantissa - Didn't work
readBuffer = Array<UInt8>( count: 1, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )
let exponent = UnsafePointer<Int8>( readBuffer ).memory

readBuffer = Array<UInt8>( count: 3, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )
let mantissa = UnsafePointer<Int16>( readBuffer ).memory

let temp = NSDecimalNumber( mantissa: mantissa, exponent: exponent, isNegative: false )
print( "TEMP: \( temp )" )

// Attempt 5 - Invert bytes - Doesn't work
readBuffer = Array<UInt8>( count: 4, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )
let exponentBuffer = [ readBuffer[ 3 ] ]
let mantissaBuffer = [ readBuffer[ 2 ], readBuffer[ 1 ], readBuffer[ 0 ] ]
let exponent = UnsafePointer<Int16>( exponentBuffer ).memory
let mantissa = UnsafePointer<UInt64>( mantissaBuffer ).memory
let temp = NSDecimalNumber( mantissa: mantissa, exponent: exponent, isNegative: false )
print( "TEMP: \( temp )" )

// Attempt 6 - Tried a bitstream frontwards and backwards - Doesn't work
readBuffer = Array<UInt8>( count: 4, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )

var bitBuffer: [String] = Array<String>( count:4, repeatedValue: "" )
for var i = 0; i < bitBuffer.count; i++ {
  bitBuffer[ i ] = String( readBuffer[ i ], radix: 2 )
  bitBuffer[ i ] = String( count: 8 - bitBuffer[ i ].characters.count, repeatedValue: Character( "0" ) ) + bitBuffer[ i ]
  //bitBuffer[ i ] = String( bitBuffer[ i ].characters.reverse() )
}
print( "TEMP: \( bitBuffer )" )

// Attempt 7 - More like the Obj. C code - Doesn't work
readBuffer = Array<UInt8>( count: 4, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )
let value = UnsafePointer<UInt32>( readBuffer ).memory
let tempData = CFSwapInt32LittleToHost( value )

let exponent = tempData >> 24
let mantissa = tempData & 0x00FFFFFF

if ( tempData == 0x007FFFFF ) {
  print(" *** INVALID *** ")
  return
}

let tempValue = Double( mantissa ) * pow( 10.0, Double( exponent ) )
print( "TEMP: \( tempValue )" )

// Attempt 8 - Saw that BLE spec says "NO Exponent" - Doesnt' work
readBuffer = Array<UInt8>( count: 1, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )

readBuffer = Array<UInt8>( count: 3, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )
let tempValue = UnsafePointer<Float>( readBuffer ).memory
print( "TEMP: \( tempValue )" )

// Example data:
// ["00110110", "00000001", "00000000", "11111111"]
//
// Only the first byte appears to ever change.
*/


// Timestamp - Year - works
readBuffer = Array<UInt8>( count: 2, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )
let year = UnsafePointer<UInt16>( readBuffer ).memory

// Timestamp Remainder - works
readBuffer = Array<UInt8>( count: 5, repeatedValue: 0 )
stream.read( &readBuffer, maxLength: readBuffer.count )
let month = readBuffer[ 0 ]
let day = readBuffer[ 1 ]
let hour = readBuffer[ 2 ]
let minute = readBuffer[ 3 ]
let second = readBuffer[ 4 ]
print( "TIMESTAMP: \( month )/\( day )/\( year ) \( hour ):\( minute ):\( second )" )

I've found this example in Objective C, which I don't know (https://github.com/AngelSensor/angel-sdk/blob/b7459d9c86c6a5c72d8e58b696345b642286b876/iOS/SDK/Services/HealthThermometer/ANHTTemperatureMeasurmentCharacteristic.m), and I've tried to work from it, but it's not clear to me what exactly is going on:

    // flags
    uint8_t flags = dataPointer[0];
    dataPointer++;

    // temperature
    uint32_t tempData = (uint32_t)CFSwapInt32LittleToHost(*(uint32_t *)dataPointer);
    dataPointer += 4;

    int8_t  exponent = (int8_t)(tempData >> 24);
    int32_t mantissa = (int32_t)(tempData & 0x00FFFFFF);

    if (tempData == 0x007FFFFF) {
        return;
    }

    float tempValue = (float)(mantissa*pow(10, exponent));

If someone could help me out with how to pull the flags and thermometer measurements from this BLE Characteristic, I would be very grateful. Thanks.

I was asked to give sample data below. Here's my sample data (12 bytes total):

["00000010", "00110011", "00000001", "00000000", "11111111", "11100000", "00000111", "00000100", "00001111", "00000001", "00000101", "00101100"]

-OR-

<025e0100 ffe00704 0f11150f>

回答1:

It can be a bit tricky to get around sometimes, but here's my simple implementation, hope it helps you out

private func parseThermometerReading(withData someData : NSData?) {
    var pointer = UnsafeMutablePointer<UInt8>(someData!.bytes)
    let flagsValue = Int(pointer.memory) //First 8 bytes are the flag

    let temperatureUnitisCelsius = (flagsValue & 0x01) == 0
    let timeStampPresent         = (flagsValue & 0x02) > 0
    let temperatureTypePresent   = ((flagsValue & 0x04) >> 2) > 0

    pointer = pointer.successor() //Jump over the flag byte (pointer is 1 bytes, so successor will automatically hot 8 bits), you can also user pointer = pointer.advanceBy(1), which is the same

    let measurementValue : Float32 = self.parseFloat32(withPointer: pointer) //the parseFloat32 method is where the IEEE float conversion magic happens
    pointer = pointer.advancedBy(4) //Skip 32 bits (Since pointer holds 1 byte (8 bits), to skip over 32 bits we need to jump 4 bytes (4 * 8 = 32 bits), we are now jumping over the measurement FLOAT

    var timeStamp : NSDate?

    if timeStampPresent {
        //Parse timestamp
        //ParseDate method is also a simple way to convert the 7 byte timestamp to an NSDate object, see it's implementation for more details
        timeStamp = self.parseDate(withPointer: pointer)
        pointer = pointer.advancedBy(7) //Skip over 7 bytes of timestamp
    }

    var temperatureType : Int = -1 //Some unknown value

    if temperatureTypePresent {
        //Parse measurement Type
        temperatureType = Int(pointer.memory))
    }
}

Now to the little method that converts the bytes to a IEEE Float

internal func parseFloat32(withPointer aPointer : UnsafeMutablePointer<UInt8>) -> Float32 {
    // aPointer is 8bits long, we need to convert it to an 32Bit value
    var rawValue = UnsafeMutablePointer<UInt32>(aPointer).memory //rawValue is now aPointer, but with 32 bits instead of just 8
    let tempData = Int(CFSwapInt32LittleToHost(rawValue)) //We need to convert from BLE Little endian to match the current host's endianness

    // The 32 bit value consists of a 8 bit exponent and a 24 bit mantissa
    var mantissa : Int32  = Int32(tempData & 0x00FFFFFF) //We get the mantissa using bit masking (basically we mask out first 8 bits)

    //UnsafeBitCast is the trick in swift here, since this is the only way to convert an UInt8 to a signed Int8, this is not needed in the ObjC examples that you'll see online since ObjC supports SInt* types
    let exponent = unsafeBitCast(UInt8(tempData >> 24), Int8.self)
    //And we get the exponent by shifting 24 bits, 32-24 = 8 (the exponent)
    var output : Float32 = 0

    //Here we do some checks for specific cases of Negative infinity/infinity, Reserved MDER values, etc..
    if mantissa >= Int32(FIRST_RESERVED_VALUE.rawValue) && mantissa <= Int32(ReservedFloatValues.MDER_NEGATIVE_INFINITY.rawValue) {
        output = Float32(RESERVED_FLOAT_VALUES[mantissa - Int32(FIRST_S_RESERVED_VALUE.rawValue)])
    }else{
        //This is not a special reserved value, do the normal mathematical calculation to get the float value using mantissa and exponent.
        if mantissa >= 0x800000 {
            mantissa = -((0xFFFFFF + 1) - mantissa)
        }
        let magnitude = pow(10.0, Double(exponent))
        output = Float32(mantissa) * Float32(magnitude)
    }
    return output
}

And here is how the date is parsed into an NSDate object

internal func parseDate(withPointer aPointer : UnsafeMutablePointer<UInt8>) -> NSDate {

    var bytePointer = aPointer //The given Unsigned Int8 pointer
    var wordPointer = UnsafeMutablePointer<UInt16>(bytePointer) //We also hold a UInt16 pointer for the year, this is optional really, just easier to read
    var year        = Int(CFSwapInt16LittleToHost(wordPointer.memory)) //This gives us the year
    bytePointer     = bytePointer.advancedBy(2) //Skip 2 bytes (year)
    //bytePointer     = wordPointer.successor() //Or you can do this using the word Pointer instead (successor will make it jump 2 bytes)

    //The rest here is self explanatory
    var month       = Int(bytePointer.memory)
    bytePointer     = bytePointer.successor()
    var day         = Int(bytePointer.memory)
    bytePointer     = bytePointer.successor()
    var hours       = Int(bytePointer.memory)
    bytePointer     = bytePointer.successor()
    var minutes     = Int(bytePointer.memory)
    bytePointer     = bytePointer.successor()
    var seconds     = Int(bytePointer.memory)

    //Timestamp components parsed, create NSDate object
    var calendar            = NSCalendar.currentCalendar()
    var dateComponents      = calendar.components([.Year, .Month, .Day, .Hour, .Minute, .Second], fromDate: NSDate())
    dateComponents.year     = year
    dateComponents.month    = month
    dateComponents.day      = day
    dateComponents.hour     = hours
    dateComponents.minute   = minutes
    dateComponents.second   = seconds

    return calendar.dateFromComponents(dateComponents)!
}

This is pretty much all the tricks you will also need for any other BLE characteristic that uses a FLOAT type



回答2:

I've done some stuff similar to you... I'm not sure if this still relevant to you, but let's dig it... Maybe my code could give you some insight:

First, get the NSData to an array of UInt8:

let arr = Array(UnsafeBufferPointer(start: UnsafePointer<UInt8>(data.bytes), count: data.length))

The spec that we are following says that the first 3 positions in this array will be representing the mantissa and the last one, will be the exponent (in the range -128..127):

    let exponentRaw = input[3]
    var exponent = Int16(exponentRaw)

    if exponentRaw > 0x7F {
        exponent = Int16(exponentRaw) - 0x100
    }

    let mantissa = sumBits(Array(input[0...2]))

    let magnitude = pow(10.0, Float32(exponent))
    let value = Float32(mantissa) * magnitude

... auxiliary function:

func sumBits(arr: [UInt8]) -> UInt64 {
    var sum : UInt64 = 0
    for (idx, val) in arr.enumerate() {
        sum += UInt64(val) << ( 8 * UInt64(idx) )
    }
    return sum
}