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.
Related
I am currently developing an app that is gonna be used for connecting with BLE devices. Everything works fine for the most part, but I have noticed a weird behaviour from the CBCentralManager class. The longer it runs a search for BLE peripherals, the less often it can actually find the same peripheral (I am talking about discovering advertisement packages). I have tried some 3rd party apps (BLE scanners etc.), and they can find my peripheral without any problems. It does advertise every X seconds, and the apps can usually find it after 1-3 * X. Here is a very simple implementation of the BLE discovery code that shows the discovery deterioration symptoms:
import CoreBluetooth
class BluetoothTestClass: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
private let manager: CBCentralManager
override init() {
self.manager = CBCentralManager(delegate: nil, queue: .main, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true])
super.init()
self.manager.delegate = self
}
func startSearch() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.manager.scanForPeripherals(withServices: nil, options: [CBCentralManagerOptionShowPowerAlertKey:true,
CBCentralManagerScanOptionAllowDuplicatesKey:true])
}
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
}
var peripherals: [CBPeripheral] = []
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
peripherals.append(peripheral)
print(Date())
}
}
The only thing that I found that helps at this point, is resetting the search every 30 seconds, then the search results are pretty close to what I can see in the 3rd party scanner apps (peripheral found after 1-3 * X seconds).
Any leads, ideas and even breadcrumbs on this topic will be highly appreciated.
The reason why the discovery rate deteriorates as the search continues is likely due to the internal state of the CBCentralManager object. Over time, it may build up internal data structures or allocate more memory, impacting its performance.
Resetting the search every 30 seconds can help to alleviate this issue, as it discards the internal state of the CBCentralManager and starts with a clean slate. Additionally, you may consider using a different dispatch queue for running the BLE discovery code.
Another thing that you may consider is limiting the duration of the scan, as the longer, the scan continues, the more the discovery performance may deteriorate. You can set the scan timeout option when calling scanForPeripherals(withServices:options:) to limit the duration of the scan.
Context
My peripheral has a characteristic which is used for sending commands but also for reading different values by sending write commands.
You can interpret these commands as I would say...
... "Hey peripheral, next time I read your characteristic xyz I am interested in the following attribute..."
Example workflow on that characteristic is:
Send read command by using peripheral.writeValue(data, for: characteristic, type: .withResponse)
Retrieve response of read command by calling peripheral.readValue(for: characteristic)
Reading value of the characteristic in callback peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?). This is my response I have to validate. Here I need to identify what command I did send in 1). But since the characteristic is always the same and also the characteristics value does not give a hint on the previous write I have no clue how to deal with this.
Question
How can I match the events I receive in didUpdateValueFor to the initial "write" call?
What I thought about as solution:
Create a simple FIFO queue but I guess I can't rely on the call order and this also does not handle possible error cases (write did fail)
Making the whole process synchronous (maybe by using actor with Swift 5.5?) but I guess this would give me other drawbacks concerning notify values from other characteristics...
Cheers 🍻
Orlando
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.
In a CoreBluetooth related iOS app written in Swift (4.2), I have the following problem.
The app handles a certain number of Peripherals and Centrals.
Each Peripheral provides a service and has several Centrals connected to it; it keeps an accounting of that and knows how many Centrals are connected.
In the same way each Central is connected to several Peripherals; and also keeps an accounting of that to know how many Peripherals are connected.
The question I have is about the book-keeping to maintain the accounting I was just mentioning, up to date.
From the Peripheral side, it works and I can see when a Central has disconnected.
For that I use:
peripheralManager(_:central:didSubscribeTo:)
and:
peripheralManager(_:central:didUnsubscribeFrom:)
From the Central I want to use:
centralManager(_:didConnect:)
and:
centralManager(_:didDisconnectPeripheral:error:)
But here, for some reason, it does not work. The first function (centralManager(_:didConnect:)) is called as expected but not the second (centralManager(_:didDisconnectPeripheral:error:)).
I suppose this last issue of the function not being called is what I need to solve. But I may be wrong.
As a result, I see when a Peripheral is coming but not when it is leaving.
What am I missing? Or am I just doing it wrong?
I test the app using two devices.
--- Further information --- (Post update)
Here is some of the related code:
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber) {
print(#function)
if cbPerifList == nil {cbPerifList = [CBPeripheral]()}
if let perifIndx = cbPerifList!.index(where:{$0.identifier==peripheral.identifier}) {
central.connect(cbPerifList[perifIndx], options: nil)
} else {
peripheral.delegate = self
cbPerifList.append(peripheral)
central.connect(peripheral, options: nil)
}
}
One more possibly relevant thing I noticed is the following. Instead of switching of the peripheral by a button switch as I should normally do, I force kill the app on the device currently playing the peripheral role, then the centralManager(_:didDisconnectPeripheral:error:) function is called on the other device as I would expect it to be; and the accounting is correctly performed.
In my CoreBluetooth App
func centralManager (_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
}
Is called when ever peripheral is lost.
Maybe check your syntax of your didDisconnectPeripheral func.
I did not find a direct answer to this question.
Reading the comments seems to show there is now answer. In other words what I wanted to do sounds impossible. If some expert happens to know otherwise, please let us know.
In the meanwhile I made a work-around solution:
When shutting of one peripheral, I use one of its characteristics to set some non-sense value which is then used as hint by the central to know that the peripheral has "said good-bye"..... And it works.
I'm building a simple IOS app for interacting with a Bluetooth LE device.
Main steps ( Connecting, discovering services, discovering characteristics and reading characteristics' values ) are done rightly.
Now i'm searching for a tutorial/example for sending command to the Bluetooth LE device i'm connected on.
I'm searching in the Apple Developer Guide but i'm not finding anything.
You should check out Apple's TemperatureSensor example, specifically where it calls [servicePeripheral writeValue:data ...
When interacting with a CBCharacteristic I am going to assume it is a read/write characteristic (you can confirm this by looking at the value for properties on your characteristic).
The two main methods when interacting with a characteristic are:
func writeValue(_ data: NSData!, forCharacteristic characteristic: CBCharacteristic!, type type: CBCharacteristicWriteType)
func readValueForCharacteristic(_ characteristic: CBCharacteristic!)
Both of these methods are found on your CBPeripheral. Once you have called one of these functions you can utilize the CBPeripheralDelegate to confirm each of these actions in these delegate methods:
optional func peripheral(_ peripheral: CBPeripheral!, didWriteValueForCharacteristic characteristic: CBCharacteristic!, error error: NSError!)
optional func peripheral(_ peripheral: CBPeripheral!, didUpdateValueForCharacteristic characteristic: CBCharacteristic!, error error: NSError!)
These are the places you will look to confirm your read and write's success. (inside of your read you can retrieve the value that was read from the BLE Device off the value property on CBCharacteristic.
Keep in mind the interactions you have (what you can read and write) are entirely dependent on the BLE device you are interrogating/interacting with. Essentially you must know what to read, what to write, and how.
You can send a data with writeValue:forCharacteristic:type: method of CBPeripheral. What characteristic+data match the required commands must be described in device specification.