19

I have a characteristic value which contains the data for an image. In the peripheral I setup the value like this:

_photoUUID = [CBUUID UUIDWithString:bPhotoCharacteristicUUID];
_photoCharacteristic = [[CBMutableCharacteristic alloc] initWithType:_photoUUID
                                                          properties:CBCharacteristicPropertyRead
                                                               value:Nil
                                                         permissions:CBAttributePermissionsReadable];

My understanding is that when this value is requested, the didReceiveReadRequest callback will be called:

-(void) peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request {

    if ([request.characteristic.UUID isEqual:_photoUUID]) {
        if (request.offset > request.characteristic.value.length) {
            [_peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
            return;
        }
        else {
            // Get the photos
            if (request.offset == 0) {
                _photoData = [NSKeyedArchiver archivedDataWithRootObject:_myProfile.photosImmutable];
            }
        
            request.value = [_photoData subdataWithRange:NSMakeRange(request.offset, request.characteristic.value.length - request.offset)];
            [_peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
        }
    }
}

This comes pretty much from Apple's documentation. On the Central side in the didDiscoverCharacteristic callback I have the following code:

if ([characteristic.UUID isEqual:_photoUUID]) {
    _photoCharacteristic = characteristic;
    [peripheral readValueForCharacteristic:characteristic];
}

Which in turn calls the didUpdateValueForCharacteristic callback:

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    NSLog(@"updated value for characteristic");

    if ([characteristic.UUID isEqual:_photoUUID]) {
        NSArray * photos = [NSKeyedUnarchiver unarchiveObjectWithData:characteristic.value];
    }
}

All of the callbacks are called but when I try to re-construct the array, it's corrupted because not all of the data is transferred correctly. I would expect the didRecieveReadRequest callback to be called multiple times with a different offset each time. However it's only called once.

I was wondering if anyone knew what I'm doing wrong?

Olivia Stork
  • 4,660
  • 5
  • 27
  • 40
James Andrews
  • 3,257
  • 3
  • 31
  • 45
  • Does the NSData has the same length (before sending and after receiving)? – Larme Oct 10 '13 at 07:28
  • NSData starts of with a length of about 7000 and finishes with a length of about 20. – James Andrews Oct 10 '13 at 15:13
  • I've got something similar going on, except when I try to respondToRequest with anything longer than 21 bytes, I get an error in didUpdateValueForCharacteristic with Error Domain=CBATTErrorDomain Code=11 "The attribute is not long." – Sandy Chapman Jan 27 '14 at 18:46

2 Answers2

36

I'm guessing you're bumping up against the 512 byte limit on characteristic length. You'll need to move to subscriptions to characteristics and processing of updates to get around this:

On the central:

  1. Subscribe to the characteristic by calling -[CBPeripheral setNotifyValue:forCharacteristic] (with YES as the notify value).

  2. In -peripheral:didUpdateValueForCharacteristic:error, every update will either be data to append, or something you choose to use on the peripheral side to indicate end-of-data (I use an empty NSData for this). Update your -peripheral:didUpdateValueForCharacteristic:error code so that:

    • If you're starting to read a value, initialize a sink for the incoming bytes (e.g. an NSMutableData).
    • If you're in the middle of reading a value, you append to the sink.
    • If you see the EOD marker, you consider the transfer complete. You may wish to unsubscribe from the characteristic at this state, by calling -[CBPeripheral setNotifyValue:forCharacteristic] with a notify value of NO.
  3. -peripheral:didUpdateNotificationStateForCharacteristic:error: is a good spot to manage the initialization and later use of the sink into which you read chunks. If characteristic.isNotifying is updated to YES, you have a new subscription; if it's updated to NO then you're done reading. At this point, you can use NSKeyedUnarchiver to unarchive the data.

On the peripheral:

  1. In -[CBMutableCharacteristic initWithType:properties:value:permissions], make sure the properties value includes CBCharacteristicPropertyNotify.

  2. Use -peripheralManager:central:didSubscribeToCharacteristic: to kick off the chunking send of your data, rather than -peripheral:didReceiveReadRequest:result:.

  3. When chunking your data, make sure your chunk size is no larger than central.maximumUpdateValueLength. On iOS7, between an iPad 3 and iPhone 5, I've typically seen 132 bytes. If you're sending to multiple centrals, use the least common value.

  4. You'll want to check the return code of -updateValue:forCharacteristic:onSubscribedCentrals; if underlying queue backs up, this will return NO, and you'll have to wait for a callback on -peripheralManagerIsReadyToUpdateSubscribers: before continuing (this is one of the burrs in an otherwise smooth API, I think). Depending upon how you handle this, you could paint yourself into a corner because:

  5. If you're constructing and sending your chunks on the same queue that the peripheral is using for its operations, AND doing the right thing and checking the return value from -updateValue:forCharacteristic:onSubscribedCentrals:, it's easy to back yourself into a non-obvious deadlock. You'll either want to make sure that you yield the queue after each call to -updateValue:forCharacteristic:onSubscribedCentrals:, perform your chunking loop on a different queue than the peripheral's queue (-updateValue:forCharacteristic:onSubscribedCentrals: will make sure its work is done in the right place). Or you could get fancier; just be mindful of this.

To see this in action, the WWDC 2012 Advanced Core Bluetooth video contains an example (sharing VCards) that covers most of this. It doesn't however, check the return value on the update, so they avoid the pitfalls in #4 altogether.

Hope that helps.

Cora Middleton
  • 956
  • 7
  • 13
  • I just tested this. It indeed solves the problem and it's much faster: 8s to transfer 30KB, against 20s for reading from characteristics. But WHY is it so much faster ? – André Fratelli Jul 01 '15 at 23:38
  • Hello Justin would I be able to grab your email so that I can send over my program. I am having this exact issue and have been over your answer multiple times but still no luck! – maxisme Aug 08 '15 at 00:03
  • Hi Maximilian, I don't want to share my email here. However, I'm jrmiddle on Github. Feel free to add me to a private repo or gist. – Cora Middleton Aug 08 '15 at 02:41
  • @JustinMiddleton, am I right that in your answer statements 4 and 5 for peripheral, corebluetooth allow updating long characteristic without readRequest, only looping through chunks and invoking updateValue with pitstop at isReadyToUpdateSubscribers? – gaussblurinc Feb 03 '16 at 12:56
1

I tried the approach described by Cora Middleton, but couldn't get it to work. If I understand her approach correctly, she would send all partial data through the update notifications. The problem for me seemed to be that there was no guarantee each update would be read by the central if the values in these notifications would change often in short succession.

So because that approach didn't work, I did the following:

  • There's some characteristic that I use to keep track of the state of the peripheral. This characteristic would only contain some flags and would send out notifications if one or more flags change. Interactions by the user on the peripheral would change the state and there's one action on the peripheral that the user can perform to trigger a download from a connected central.

  • The data to be downloaded from the central is added to a stack on the peripheral. The last item on the stack is a terminator indicator (an empty NSData object)

  • The central registers to receive notifications of the aforementioned state characteristic. If some flag is set, a download is triggered.

  • On the peripheral side, every time I receive a read request for a certain characteristic, I remove 1 item from the stack and return this item.

  • On the central side I add all data that is returned from the read requests. If the empty data value is retrieved, then I create an object from the returned data (in my case it's a JSON string).

  • On the peripheral side I also know the download is finished after returning the empty NSData object, so afterwards I can change the state once again for the peripheral.

Wolfgang Schreurs
  • 11,779
  • 7
  • 51
  • 92