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.
Related
When doing Bluetooth communications one is often placed in a situation where one does a call which gets a response in a delegate, for example the characteristic discovery shown below:
func discoverCharacteristics(device: CBPeripheral)
{
servicesCount = device.services!.count
for service in device.services!
{
print("Discovering characteristics for service \(service.uuid)")
device.discoverCharacteristics([], for: service)
}
}
Now this discovery is not for a specific device but for health devices following the Bluetooth SIG services/profiles, so I don't know exactly what services they might have and I don't know how many characteristics might be in each service. The method is asynchronous and the answers are signaled in the following delegate method:
// Discovered Characteristics event
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
{
for characteristic in service.characteristics!
{
print("Found characteristic \(characteristic.uuid)")
}
servicesCount = servicesCount - 1;
print("Characteristics sets left: \(servicesCount)")
if servicesCount == 0
{
print ("Found all characteristics")
DispatchQueue.main.async{
self.btleManager!.btleManagerDelegate.statusEvent(device: peripheral, statusEvent: Btle.CHARACTERISTICS_DISCOVERED)
}
self.device = peripheral
self.handleMds()
}
}
Now I need to wait until the discovery is done before I can take the next step because what I do next often depends on what I got. In Java and on Android what I do is wait on a CountDownLatch in the calling method and I signal the latch in the callback to release that wait.
The iOS equivalent of the CountDownLatch seems to be the DispatchSemaphore. However, doing so apparently blocks the system and no delegate ever gets called. SO what I have done (as shown in the code above) is to initialize a variable servicesCount with the number of services and in the delegate callback decrement it each time it is signaled. When it gets to zero I am done and then I do the next step.
This approach works but it seems hacky; it can't be correct. And its starting to get really messy when I need to do several reads of the DIS characteristics, the features, the various time services, etc. So what I would like to know is what is the proper method to wait for a delegate to get signaled before moving forward? Recall I do not know what services or characteristics these devices might have.
First of all, if you already have an implementation with CountDownLatch for Android, you may just do the same implementation for iOS. Yes, Swift doesn't have a built-in CountDownLatch, but nice folks from Uber created a good implementation.
Another option is to rely on a variable, like you do, but make it atomic. There are various implementations available online, including one in the same Uber library. Another example is in RxSwift library. There are many other variations. Atomic variable will provide a thread-safety to variable's read/write operations.
But probably the most swifty way is to have a DispatchGroup. Will look something like this:
let dispatchGroup = DispatchGroup() // instance-level definition
// ...
func discoverCharacteristics
{
for service in device.services!
{
dispatchGroup.enter()
// ...
}
dispatchGroup.notify(queue: .main) {
// All done
print ("Found all characteristics")
DispatchQueue.main.async{
self.btleManager!.btleManagerDelegate.statusEvent(device: peripheral, statusEvent: Btle.CHARACTERISTICS_DISCOVERED)
}
self.device = peripheral
self.handleMds()
}
// ...
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
{
// ...
dispatchGroup.leave()
}
In other words, you enter the group when you are about to submit a request, you leave it when request is processed. When all items left the group, notify will execute with the block you provided.
You only get a single call to didDiscoverCharacteristicsFor for each call to device.discoverCharacteristics. That is, the didDiscoverCharacteristicsFor call is only made once all of the service's characteristics have been discovered.
You have the peripheral passed to the delegate call, so you have the information you need to know the context of the delegate call. There is no need to "wait". You can use a simple state machine per peripheral or service if you are discovering data for multiple peripherals/services simultaneously.
You can even keep a set of peripherals that aren't in a completely discovered state if you need to take some action once discovery is complete; e.g. You can remove a peripheral from the set once discovery is complete for that peripheral. If the set is empty, discovery is complete for all devices.
All of this state belongs in your model. All your delegate implementation should need to do is update the model with the data that has been discovered.
I just started to study about RxBluetoothKit as easy solution to interact with BLE devices and I have very basic knowledge of Rx programing.
As i can see from examples, every time i have to write some characteristic i have to scan + establishConnection to Peripheral + discover Services and only then write and subscribe for confirmation of this specific Characteristic.
Same happen for read Characteristic.
If I understand correctly, this way I can subscribe only to one sequence/ connection at same time.
But what i need is to subscribe to Bluetooth state and to Peripheral connection state and to notify Characteristic, in addition i have send write commands to same Peripheral sometimes.
Need help to understand how should i handle this scenario by using RXBluetoothKit library?
Links to similar approachment on GitHub are welcomed.
Thank you!
This exact case isn't covered by RxBluetooth kit, so you'll have to manage this case by yourself. Not the most ideal, but you could go with something like this:
// Get an observable to the Peripheral, then share it so
// it can be used for multiple observing chains
let connectedPeripheral: Observable<Peripheral> = peripheral
.establishConnection()
.share(replay: 1, scope: .whileConnected)
// Establish a subscription to read characteristic first
// so no notifications are lost
let readDisposable = connectedPeripheral
.flatMap { $0.observeValueAndSetNotification(for: Characteristic.read) }
.subscribe()
// Write something to the write characteristic and observe
// responses in the chain above
let writeDisposable = connectedPeripheral
.flatMap { $0.writeValue(data, for: Characteristic.write, type: .withResponse) }
.subscribe()
The example above is just a gist, but the general idea should work since I'm doing a similar thing in a project of my own. Be careful to dispose the observables when done, either by .take or disposeBags.
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.
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 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.