I'm having trouble within an IOS swift application trying to get bluetooth RSSI signal strength from the peripheral. I've been trying to use readRSSI() (see code below) which returns a Future, but I've so far been unable to map that Future into another usable variable such as an Int or String. I'm new to Swift, so not sure if I'm missing an async step or other. I'm used to working in R, python, JS and having some challenges wrapping my head around the syntax. Any help is greatly appreciated.
I've tried switching multiple ways of extracting the content from within extensions to the ViewController without luck. I get errors on type-mismatches no matter how I try to pass the Future type value.
let strengthCharacteristic = self.peripheral.readRSSI()
let thisRet = self.strengthChar.map({ avar in
return avar
})
self.strengthLabel.text = String(thisRet ?? 0)
Apple docs for readRSSI() method:
On iOS and tvOS, when you call this method to retrieve the RSSI of the peripheral while connected to the central manager, the peripheral calls the peripheral:didReadRSSI:error: method of its delegate object, which includes the RSSI value as a parameter.
In order to read the RSSI from a peripheral, implement the peripheral:didReadRSSI:error: in the delegate object of the peripheral. This method will be fired when you call the readRSSI() method of the peripheral object. You can retrieve the RSSI value directly in this method as an input parameter. Don't forget to assign the delegate to the peripheral object.
Related
I have several custom BLE LED lights (peripherals). I've implemented a bluetooth helper class (CBCentralManager and associated delegate methods), and can connect and control each light individually without issue. I have handled all of the basic retrieve / connect / notify logic, and it works great.
My next step was to connect to multiple lights and control them essentially simultaneously (or as near as possible). To manage the different active peripherals, I created a dictionary to store them in, using the UUID as the key like so (BLEDevice contains a peripheral property where I store the reference):
class BLEDevice {
var uuid: String!
var peripheral: CBPeripheral!
var rssi: NSNumber!
var name: String!
var advertData: [String: Any]!
var type = 0
init() {
}
}
var bleDeviceDict: [String: BLEDevice] = [:]
This dictionary gets populated during the retrieval process, and then I do a connection request for each one. Upon success of connection, service discovery, characteristic discovery...etc, I update the dictionary object. I'm able to connect to each device without any issue, but when I attempt to send any commands (with or without a response flag) I get unreliable operation. Sometimes it works great, and other times it throws the following error:
[CoreBluetooth] WARNING: <CBCharacteristic: 0x129701430, UUID = FFE1, properties = 0x1C, value = <45080154>, notifying = YES> is not a valid characteristic for peripheral <CBPeripheral: 0x127e0ad70, identifier = 44CDB018-2A5C-D776-7641-7F193470945A, name = 3CA5090A21AFED, state = connected>
To send the group commands, I set up a basic for loop to iterate through and send the command to each peripheral, and I'm almost certain this brute force approach is the wrong way to accomplish this. I've done some searching, but have been unable to find any literature on the proper way to control devices at the same time. I could really use some advice or direction to help solve this problem. What is the best practice for achieving this behavior?
You need to store a specific instance of the relevant characteristic for each peripheral you are connected to.
Add properties to your BLEDevice to store references to the relevant characteristics you find during discovery.
There is no other way to send the data to each of the peripherals; You need to iterate over your peripheral collection and write the data to each one.
I am working with iOS 8's Core Bluetooth API. I have written some simple Swift code that creates a Central Manager and that implements the CBManager & CBPeripheral protocols. When the callbacks are executed they perform logging:
2015-09-04 14:08:33.719 CentralManager - Did update state to Powered on
2015-09-04 14:08:33.745 CentralManager - Did discover peripheral: CBPeripheral: 0x1700f5080, identifier = 3DD113C3-D175-7374-CF88- F471BB470169, name = Apple TV, state = disconnected
2015-09-04 14:08:33.955 CentralManager - Did connect peripheral: CBPeripheral: 0x1700f5080, identifier = 3DD113C3-D175-7374-CF88-F471BB470169, name = Apple TV, state = connected
2015-09-04 14:08:34.462 Peripheral - Did discover service: CBService: 0x174075600, isPrimary = YES, UUID = Continuity
2015-09-04 14:08:34.582 Peripheral: Did discover characteristic for service Continuity : ()
Everything looks good. As you can see, log statements 2 and 3 print the value of the CBPeripheral argument that was passed to the callback function. I am using Swift's string interpolation capability to accomplish this - ex. "The value is \(peripheral)".
Now for the problem: When I set a breakpoint in one of the callback functions and examine the CBPeripheral argument via Xcode's debug pane, I am not able to view any of the values that are revealed by the logging statements. When I expand the CBPeripheral (i.e. click on the triangle next to it) I see that it is a CBPeer. When I expand the CBPeer I see that it is an NSObject. But, no variables or properties are revealed.
Does anyone have an explanation for this?
Cheers, Robert
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?
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:
Subscribe to the characteristic by calling -[CBPeripheral setNotifyValue:forCharacteristic] (with YES as the notify value).
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.
-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:
In -[CBMutableCharacteristic initWithType:properties:value:permissions], make sure the properties value includes CBCharacteristicPropertyNotify.
Use -peripheralManager:central:didSubscribeToCharacteristic: to kick off the chunking send of your data, rather than -peripheral:didReceiveReadRequest:result:.
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.
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:
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.
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.
I have two programs, one for Mac and one for iOS. When I connect to the iOS device from the Mac, it finds the services I want, and the characteristics I want. After finding the characteristic, I use peripheral.setNotifyValue(true, forCharacteristic: characteristic), but the peripheralManager:central:didSubscribeToCharacteristic: method isn't being called on the iOS side. When I check if characteristic.isNotifying it is false. From what I understand when I set the notify value to true, it should be notifying, and whenever i change the value is updates. Why is it not updating? Thanks in advance.
Here is the code that sets up the characteristic in question -
self.characteristic = CBMutableCharacteristic(type: UUID_CHARACTERISTIC, properties: CBCharacteristicProperties.Read, value: self.dataToSend, permissions: CBAttributePermissions.Readable)
theService = CBMutableService(type: UUID_SERVICE, primary: true)
theService.characteristics = [characteristic]
self.peripheralManager.addService(theService)
If you want your characteristic to support notify operations then you need to set this on its properties -
self.characteristic = CBMutableCharacteristic(type: UUID_CHARACTERISTIC, properties: CBCharacteristicProperties.Read|CBCharacteristicProperties.Notifiy, value: self.dataToSend, permissions: CBAttributePermissions.Readable);
An attempt to set notification on a characteristic that doesn't state support for notification is ignored by Core Bluetooth.
Also, be aware that by setting a value when the characteristic is created, you will not be able to change the value of this characteristic in the future - therefore notification is somewhat pointless. If you want to be able to change the value you must specify nil for value when you create the characteristic.
From the CBMutableCharacteristic documentation -
Discussion
If you specify a value for the characteristic, the value is
cached and its properties and permissions are set to
CBCharacteristicPropertyRead and CBAttributePermissionsReadable,
respectively. Therefore, if you need the value of a characteristic to
be writeable, or if you expect the value to change during the lifetime
of the published service to which the characteristic belongs, you must
specify the value to be nil. So doing ensures that the value is
treated dynamically and requested by the peripheral manager whenever
the peripheral manager receives a read or write request from a
central. When the peripheral manager receives a read or write request
from a central, it calls the peripheralManager:didReceiveReadRequest:
or the peripheralManager:didReceiveWriteRequests: methods of its
delegate object, respectively.
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?
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:
Subscribe to the characteristic by calling -[CBPeripheral setNotifyValue:forCharacteristic] (with YES as the notify value).
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.
-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:
In -[CBMutableCharacteristic initWithType:properties:value:permissions], make sure the properties value includes CBCharacteristicPropertyNotify.
Use -peripheralManager:central:didSubscribeToCharacteristic: to kick off the chunking send of your data, rather than -peripheral:didReceiveReadRequest:result:.
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.
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:
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.
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.