Search code examples
iosobjective-cnsstringnsdata

Convert NSData to a NSString returns random characters


I am working on a bluetooth iOS project and have managed to get some data from the bluetooth device. However, I am struggling to convert this data into something useful, such as an NSString. Whenever I try to NSLog the NSString that was converted from the NSData received, it is a bunch of gibberish. The output is:

ēဥ၆䄀

The bluetooth device is a heart monitor from a manufacturer in Asia and they have provided the protocol reference on how to make calls to the device. This one thing they mention in the protocol reference:

The PC send 16-byte packets to the device, then the device sent back the 16-byte packets. Except for some special commands, all others can use this communication mode.

Can anyone tell me what I am doing wrong? I have tried everything I know, including every single encoding in the apple docs as well as both initWithData and initWithBytes. Thanks!

-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
         error:(NSError *)error {
if (error)
{
    NSLog(@"erorr in read is %@", error.description);
    return;
}
NSData *data= characteristic.value;
 NSString *myString = [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSUTF16StringEncoding];
NSLog(@"Value from device is %@", myString); //OUTPUT IS ēဥ၆䄀

}

Solution

  • What you have here is a string of raw data that can't be directly converted into a human readable string - unless you consider hex-representation to be human readable :)

    To make sense of this data you need to either have a protocol specification at hand or prepare for hours (sometimes) days of reverse-engineering.

    This byte-sequence can be composed of multiple values formatted in standard (float IEEE 754, uint8_t, uint16_t...) or even proprietary formats.

    One important thing to consider when communicating with the outside world is also endianness (ie: does the 'biggest' byte in multi-byte format come first or last).

    There are many ways to manipulate this data. To get the raw array of bytes you could do:

    NSData *rxData = ...
    uint8_t *bytes = (uint8_t *)[rxData bytes];
    

    And then if (for example) first byte tells you what type of payload the string holds you can switch like:

    switch (bytes[0])
    {
        case 0x00:
            //first byte 0x00: do the parsing
            break;
    
        case 0x01:
            //first byte 0x01: do the parsing
            break;
    
        // ...
    
        default:
            break;
    }
    

    Here would be an example of parsing data that consists of:

    byte 0: byte holding some bit-coded flags bytes 1,2,3,4: 32-bit float bytes 5,6: uint16_t

    bool bitFlag0;
    bool bitFlag1;
    bool bitFlag2;
    bool bitFlag3;
    
    uint8_t firstByte;
    float theFloat;
    uint16_t theInteger;
    
    NSData *rxData = ...
    uint8_t *bytes = (uint8_t *)[rxData bytes];
    
    // getting the flags
    firstByte = bytes[0];
    
    bitFlag0 = firstByte & 0x01;
    bitFlag1 = firstByte & 0x02;
    bitFlag2 = firstByte & 0x04;
    bitFlag3 = firstByte & 0x08;
    
    //getting the float
    
    [[rxData subdataWithRange:NSMakeRange(1, 4)] getBytes:&theFloat length:sizeof(float)];
    NSLog (@"the float is &.2f",theFloat);
    
    //getting the unsigned integer
    
    [[data subdataWithRange:NSMakeRange(6, 2)] getBytes:&theInteger length:sizeof(uint16_t)];
    NSLog (@"the integer is %u",theInteger);
    

    One note: depending on the endianness you might need to reverse the 4-float or the 2-uint16_t bytes before converting them. Converting this byte arrays can also be done with unions.

    union bytesToFloat
    {
        uint8_t b[4];
        float f;
    };
    

    and then:

    bytesToFloat conv;
    
    //float would be written on bytes b1b2b3b4 in protocol
    conv.b[0] = bytes[1]; //or bytes[4] .. endianness!
    conv.b[1] = bytes[2]; //or bytes[3] .. endianness!
    conv.b[2] = bytes[3]; //or bytes[2] .. endianness!
    conv.b[3] = bytes[4]; //or bytes[1] .. endianness!
    
    theFloat = conv.f, 
    

    If for example you know that byte6 and byte7 represent an uint16_t value you can calculate it from raw bytes:

    value = uint16_t((bytes[6]<<8)+bytes[7]);
    

    or (again - endianness):

    value = uint16_t((bytes[7]<<8)+bytes[6]);
    

    One more note: using simply sizeof(float) is a bit risky since float can be 32-bit on one platform and 64-bit on another.