CBCentralManager peripheral discovery deteriorating after a longer search - ios

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.

Related

Proper way to wait for iOS Swift CBPeripheralDelegate to complete?

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.

centralManager(_:didDisconnectPeripheral:error:) not called

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.

finding nearby bluetooth devices with swift

I am currently using swift to make an app that can generate a list of bluetooth devices nearby. However, I cannot find any documents that use swift to do that. All of the files are in Objective C and I am wondering if I can just make an objective C file and connect directly to the storyboard? (my project file is in swift).
Also, do I have to include any other library from outside? (ex: serialGATT) or coreBluetooth.framework is good enough?
You'll have to import CoreBluetooth
import CoreBluetooth
Add the CBCentralManagerDelegate to your controller. (For a simple app I attached it to my View Controller)
class ViewController: UIViewController, CBPeripheralDelegate, CBCentralManagerDelegate {
You should create a local variable centralManager (or similar) and then initialize in your viewDidLoad function
centralManager = CBCentralManager(delegate: self, queue: nil)
Finally you can create a new function called centralManagerDidUpdateState which will act as a callback when the Bluetooth state changes (it's always called on startup in your app.
// If we're powered on, start scanning
func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("Central state update")
if central.state != .poweredOn {
print("Central is not powered on")
} else {
print("Central scanning for", ParticlePeripheral.particleLEDServiceUUID);
centralManager.scanForPeripherals(withServices: [ParticlePeripheral.particleLEDServiceUUID],
options: [CBCentralManagerScanOptionAllowDuplicatesKey : true])
}
}
The important call is centralManager.scanForPeripherals This will start the scanning process. In my case I'm filtering devices that only have ParticlePeripheral.particleLEDServiceUUID in their advertising packets.
That should get you scanning and on your way. I wrote a full end-to-end tutorial on how to use Swift with Bluetooth. It will go into much more detail. Here it is.

iOS Swift - Reload location function with NSTimer for app background doesn't work

I've got a problem with location services. I can't set up a function that updates my location coordinates in the background by a NSTimer. Here is my code from appDelegate:
var locationManager = CLLocationManager()
func applicationDidEnterBackground(application: UIApplication) {
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.theTimer = NSTimer(fireDate: NSDate(), interval: 40, target: self, selector: "handleTimer", userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(self.theTimer, forMode: NSDefaultRunLoopMode)
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
var locValue:CLLocationCoordinate2D = manager.location.coordinate
println("dinBack = \(locValue.latitude) \(locValue.longitude)")
self.locationManager.stopUpdatingLocation()
}
func handleTimer(){
println("started")
self.locationManager.startUpdatingLocation()
}
PS. - Of course that i've imported corelocation.
- When I get back into the app, the console prints what should have printed in the background.
You can not make an NSTimer work like this while your application is in the background. NSTimer's are not "real-time mechanisms". From the official documentation:
Timers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see NSRunLoop and Threading Programming Guide. Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.
Emphasis mine.
The important take away from this is that while your application is in the background, any run loop that your timer would have been scheduled on is not actively running.
As soon as your app returns to the foreground, this run loop fires back up, sees that your timer is overdue, and sends the message to the selector.
With iOS 7 and forward, if you want to perform operations in the background, you can tell the OS that you want to perform "background fetches".
To set this up, we must first tell the OS how frequently we want to fetch data, so in didFinishLaunching..., add the following method:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
application.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
return true
}
We can pass any time interval here (for example, if we only want to check once a day). The value we pass in is only defining a minimum amount of time that should pass between checks, however. There is no way to tell the OS a maximum amount of time between checks.
Now, we must implement the method that actually gets called when the OS gives us an opportunity to do background work:
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
// do background work
}
We can do whatever we want within this method. There are two catches, however.
This method is called while our app is in the background. The OS limits us to (I believe) thirty seconds. After thirty seconds, our time is up.
We must call the completionHandler() (or the OS will think we used all of our time).
The completionHandler that gets passed in takes an enum, UIBackgroundFetchResult. We should pass it either .Failed, .NewData, or .NoData, depending upon what our actual results were (this approach is typically used for checking a server for fresh data).
So, our method might look like this:
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
// do stuff
if let _ = error {
completionHandler(.Failed)
} else if results.count > 0 {
completionHandler(.NewData)
} else {
completionHandler(.NoData)
}
}
Keep in mind, we have absolutely zero control over how frequently the OS will actually let us run this code in the background. The OS uses several metrics in order to optimize the user's experience.
I think if your app reports .Failed to the completion handler, the OS might give you a second chance soon, however if you're abusing .Failed, the OS could probably blacklist your application from using background fetches (and Apple could deny your app).
If your app isn't reporting .NewData, the OS will let your app do background work less often. I'm not saying this because I recommend that you just always report .NewData. You should definitely report accurately. The OS is very smart about scheduling work. If you're passing .NewData when there isn't new data, the OS will let your app work more often than it may need to, which will drain the user's battery quicker (and may lead to them uninstalling your app altogether).
There are other metrics involved in when your app gets to do background work however. The OS is very unlikely to let any app do background work while the user is actively using their device, and it is more likely to let apps do background work while the user is not using their device. Additionally, OS is more likely to do background work while it is on WiFi and while it is plugged into a charger of some sort.
The OS will also look at how regularly the user uses your app, or when they regularly use it. If the user uses your app every day at 6pm, and never at any other time, it's most likely that your app will always get an opportunity to do background work between 5:30pm and 6pm (just before the user will use the app) and never during any other part of the day. If the user very rarely uses your app, it may be days, weeks, or months between opportunities to work in the background.

Sending command to Bluetooth LE Device from a iOS App

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.

Resources