I am designing an iOS framework to handle multiple BLE devices (all of the same kind). Everything is working very well at the moment, except one thing:
The client wants a list with available devices. But how can I detect when a device, that has been discovered in the past, is not available anymore?
Another problem occurs, when I try to connect to a device that is not available anymore. Documentation says: Connection attempts never time out and
And yes, I never get an error via didFailToConnectPeripheral.
I did some research but couldn't figure out how handle these problems via CoreBluetooth properly. So I developed my own solutions, but I am not sure if that is the right way (or at least a good way, cause there may be several ways to do it).
1. Detecting devices that are not available anymore
I scan with
[_centralManager scanForPeripheralsWithServices:services options:#{CBCentralManagerScanOptionAllowDuplicatesKey: #(TRUE)}];
so I receive advertisments all the time as long as a device is not connected. I check with a timer that the advertisement reoccured in a given time interval (large enough corresponding to the devices ad interval). If the advertisement didn't occur in the interval, I remove the device from the list.
2. Detecting connection timeout
Well, that's a pretty easy one I think. I use my own timeout function and cancel the connection request if the timer expires.
If somebody ever came across these problem, I would be very interested in your opinion and/or your solution of course.
UPDATE 2014-12-17:
In the meantime I worked on my own solution using timers and it seems to work pretty well.
Connection timeout is straight forward. Simply set a timer to 5 seconds or whatever you think is good for you. If the timer expires and the device did not connect, simply cancel the connection and tell the user that there was a problem.
Detecting devices that go out of range was a bit trickier. For every discovered device I start a timer that fires after double the time, the device sends advertisements. If the device does send another advertisement till the timer expires, it probably went out of range or was turned off or connected to another device.
I don't want to answer my own question because I hope that maybe Apple will one day take care of those problems.
The correct way to determine whether a device is available is to store the peripheral identifier value. Before you attempt to reconnect, call retrievePeripheralsWithIdentifiers. However, this still does not guarantee that the device will be in range by the time you attempt to connect!
Connection attempts do not time out at the OS level, and this is explicitly documented.
Some apps may need to use the Core Bluetooth framework to perform
long-term actions in the background. As an example, imagine you are
developing a home security app for an iOS device that communicates
with a door lock (equipped with Bluetooth low energy technology). The
app and the lock interact to automatically lock the door when the user
leaves home and unlock the door when the user returns—all while the
app is in the background. When the user leaves home, the iOS device
may eventually become out of range of the lock, causing the connection
to the lock to be lost. At this point, the app can simply call the
connectPeripheral:options: method of the CBCentralManager class, and
because connection requests do not time out, the iOS device will
reconnect when the user returns home.
Related
I know the answer is nominally "no", but I mean really—what if the app goes into the background (with BTLE background processing enabled)? For 24 hours? Across an app update?
Under the heading "Reconnecting to Peripherals", this Apple documentation describes a reconnection workflow that first tries to reconnect to previously paired peripherals found via retrievePeripheralsWithIdentifiers: but then starts scanning again if you fail to connect. How do you know when to give up on connect-ing to a previously found peripheral if there is no formal timeout? How do you know when to start/keep scanning if the idea is to re-connect to a previously found BTLE device whenever you move back into proximity to it, without the user necessarily interacting with your app?
Also, a note further down that page says that some BTLE devices might invent a random identifier for themselves every time they're powered on, so even though you find some previously paired peripherals from retrievePeripheralsWithIdentifiers: you might not be able to connect to them as their names have changed. Do any BTLE devices do that in practice? That's nuts!
This is a tricky one to answer. The CoreBluetooth framework itself does not have an official timeout on connect requests. In fact it will try to connect the peripheral for as long as possible. But how long is that?
Well, unfortunately this is not something that is very well defined. You can be pretty confident that the connection will not time out while the app is in the foreground, but as soon as you involve connections in the background then things are not so funny any more. Obviously, like you mention, the pending connection will not remain after a phone reboot, etc.. which is fine since no user would expect the app to still be running after a reboot anyway. Regarding long running pending connections, you will find in Apple’s documentation that they tell you to opt-in for State Preservation and Restoration in order to make sure that the pending connections are properly kept while the app is suspended and eventually terminated. This would be good if it worked as advertised, but unfortunately it does not. After many years of working with this I have found that it is nearly impossible to get a reliable background pending connection on iOS. I have reported many bugs on this topic but so far none have been resolved.
There are a few issues in particular that I think you should pay extra attention to:
State Preservation and Restoration will completely stop working if a Bluetooth-state-change event happens while your app is in the terminated state. This essentially means that if the bluetooth chip gets reset for any reason (ex by toggling bluetooth/flight mode/etc..) then your app will never be relaunched again by Core Bluetooth whenever the peripheral is advertising within range. The reason for this is because all pending connections that have been set by your app will be cleared whenever the bluetooth chip is restarted. The problem with this is that your app will not be relaunched to be notified of this change, so the pending connections will never be recovered. So your app will think that the peripherals will connect, while in fact they will not. To me this one is the most serious issue and it alone makes CoreBluetooth extremely unreliable.
Sometimes the framework gets ”stuck” in a bad state (possibly by an internal race-condition or similar). This can happen randomly, but you can pretty easily reproduce this by calling connectPeripheral immediately in the didFailToConnect or didDisconnect callback. When this happen the ”connection state” property is set to “connecting” when a pending connection is in fact not set. To avoid this I have found that you should wait at least around 20ms before connecting, for example using a dispatch_after or something.
The framework internally uses XPC connections for interprocess communication in order to deliver bluetooth event. On some occasions this will break for whatever reason and the connection will be lost. I don’t know why this happens, but whenever it happens state preservation will stop working and you will manually have to relaunch the app to recover from it. Sometimes I manages to catch this in the device sysdiagnose logs...
Using an iPhone 7 and at the same time having an Apple Watch (paired the phone) will completely break all reconnects from behind the lock screen in case the Watch is not currently connected (out of range/flight mode/low battery/or any other reason). This is particularly bad since it was introduced recently! But it looks like the Apple Watch for some reason has "priority" over other bluetooth peripherals.
These are from the top of my head, but there are other issues as well. Regarding random addresses, most often these peripheral use so called ”random resolvable” addresses. This means that they appear random but in fact they can be resolved using an IRK (Identity Resolving Key) which is usually shared during initial bluetooth bonding. Devices that use completely random addresses are to my knowledge not very common.
we have a BLE peripheral that connects to the phone every hour and passes some data. Here is how the process works:
Upon launch with key UIApplicationLaunchOptionsBluetoothCentralsKey in
application(didFinishLaunchingWithOptions launchOptions) app re-initializes CBCentralManager with ID that was passed to it.
Then it goes through the regular restoration cycle and reads data off the BLE peripheral.
Performs REST request to the service in the cloud.
Assuming that app has been launched at least once after phone reboot everything works well for a few days (if app isnt running or been forced out of memory, iOS properly starts it up again, assuming user didnt do forced close manually).
However every few days iOS stops waking up the app when there is an incoming request from BLE device. If user relaunches app everything works properly for a few days and then stops agains. Given the nature of our product, it's critical to have our app / peripheral working together in the most reliable way possible.
Theories as to why it might be happening:
(upon closer examination all of them were dismissed)
Users restart the phone and forget to relaunch the app.
We've added logging of the uptime and it showed that phone didnt restart in between app launches.
Memory warnings lead to app being booted out.
Once again, added logging, they showed that there was no applicationDidReceiveMemoryWarning
Bad connection leads to app running for longer period than 10s when uploading results and iOS terminates it and gets upset
We artificially delayed server response by 15s to test this and everything continues to work properly during testing.
Any ideas on what is happening and why iOS stops notifying app about incoming BLE connection?
One of the problems is that we cannot figure out how to reliably reproduce the issue So any suggestions there will be much appreciated as well!
Thank you!
UPDATE 1:
Here is how we initialize CBCentralManager:
self.centralManager = CBCentralManager(delegate: self, queue: nil, options: [
CBCentralManagerOptionRestoreIdentifierKey : MyCentralManagerID,
CBCentralManagerOptionShowPowerAlertKey : 0])
I saw some suggestions that queue parameter should not be nil. Given that I'm unable to reliably reproduce issue I'm hesitant to make that change until I can confidently observe its effects.
I wanna start by saying that I have been working with CoreBluetooth for a long time now and from what I have noticed CoreBluetooth State Preservation and Restoration does not work reliably at all. You can get it working sort of "ok", but you will never get it to reconnect reliably unless Apple fixes it some day.
There are so many bugs that causes this to not work properly, but I will give you one that I believe is causing your problems:
State restoration will only relaunch your app due to bluetooth related activity if the event originates from a peripheral accessory that you are communicating with, such as connect/disconnect events and characteristics notifications. For other events, most importantly general bluetooth state change events, your app will not be relaunched and notified of this. The reason why this is so bad is because all bluetooth state change events will cancel all pending or current connections, meaning that pending connections will be dropped and your application will not be notified of it. This effectively means that your application will still believe that the connections are still pending when in fact they are not. Since your application is terminated at this time, the only way for it to wake up again is by having the user manually launch it again (or alternatively “hack” other background modes for this purpose, which does not work very reliably either).
This thing happens if the user toggles Flight Mode, toggles Bluetooth, power cycles the iOS device, or any other undefined reasons that many cause state changes…
But this is only one bug. Many other exists as well, such as the XPC connection being interrupted at different times for no apparent reason. I have also noticed that the pending connection can go into “limbo” mode where the peripheral state gets set to Connecting, but in fact it will never connect unless you cycle the connection state.
Anyhow, I am sad to say it, but if you are developing an app that must rely on the peripheral being reconnected in the background then I would not recommend doing it. You will be frustrated. I could probably write an essay about all the bugs in Core Bluetooth that Apple does not want to fix. Even more strange is that you can pretty easily ruin the bluetooth connectivity globally on the device from one single app so that no app can use bluetooth until the device is rebooted. This is pretty bad since it goes against Apple's own Sandboxing principle.
Sorry for the long title, but we are having a pretty interesting issue with using corebluetooth for ios. We are issuing a call to retrievePeripherals in CBCentralManager and are able to find the previously paired device.
This happens though regardless if the device is on or off though. I can't find anything in apple's documentation as to why it's able to find the device when it is off though and it isn't showing up in Settings -> Bluetooth -> Devices. I'm suspecting that Apple is caching this information but can't find any documentation to confirm this. Also, when the device is off and we issue the connect call, the program continues to execute as normal but the delegate for didFailToConnect never gets called. When the device is turned on, it will connect immediately.
Is there a way to pass a timeout parameter when trying to connect to a device? If not, what would the best solution be to handling reconnecting to a previously used device for an application (we're storing the last connected device within the app).
Two points you need to know about retrievePeripherals: and connectPeripheral:
1.) retrievePeripherals: attempts to retrieve the CBPeripheral object associated with the uuid you supply. Even if the ble device is off (or on the other side of the country) retrievePeripherals: will still return an instance of CBPeripheral that you can call connectPeripheral: on. This is done intentionally so that you can issue a call to a peripheral that is not even around and still automatically connect to it when it comes back into range. It basically creates a marker inside the system bluetooth so that when the device is actually seen, it will know it should connect to it.
2.)connectPeripheral: will not time out unless the communication channel is broken with the actual device. If the iOS device has not seen the device, it will not fail and should not time out (unless some error occurs inside the system bluetooth).
And as for the timeout parameter, there is no documented way inside the CoreBluetooth framework. You can create your own implementation for it, however I believe you'd be better off keeping a list of which peripheral uuid's you've actually called connectPeripheral: on and then just pop them from the list when they connect. If you no longer want to connect to a peripheral in the list call cancelPeripheral: on that UUID, call connectPeripheral: on the other, and swap entries. Good to go.
I've looked everywhere and tried everything, but nothing seems to work :(
On iOS, I'm making an app (for iOS 6 and above) in which iOS devices need to exchange data. Therefore, both devices need to be peripheral and central at the same time. I've done exactly as specified in the WWDC video, but the devices can't connect successfully with each other.
When I make one device only central and the other only peripheral, the central connects seamlessly to the peripheral.
However, when both devices are peripheral and central at the same time, I get random errors: at any stage (discovering services/characteristics or setting notify value to YES) errors sometimes happen, and sometimes discoverServices doesn't even call didDiscoverServices
Is there something different I should be doing? I simply merged the peripheral and central code into one view controller. I've noticed that if device "a" connects to device "b", and then device "b" connects to device "a", it works more often than not. I manage this by using NSThread sleepForTimeInterval: manually for different amounts of time on each device, but how could I get one device to connect first (and then the other) in a reliable (and not manually pre-defined) way?
If I do get errors, usually they're simply Unknown error
Please let me know if you need any code or any other information :)
Yes, it can be in both roles at the same time. You just have to initialize a CBPeripheralManager and a CBCentralManager. As soon as the peripheral manager is initialized and you receive the POWER ON state the device starts acting as a peripheral. You can add your services at this point and receive connections from other devices. At the same time you can use the central manager to scan and initiate connections to other peripherals.
Note that you cannot connect to your own device even if it acts as a peripheral.
For your errors, I suggest:
Turn off scanning before initiating a connection. That is, scan, find peripheral, stop scan, connect. Connection and scanning do not like each other.
Use a dedicated queue for handling bluetooth events, not the main queue. [[CBCentralManager alloc] initWithDelegate:self queue:my_dedicated_bluetooth_q]
Unfortunately, the stack sometimes become unstable. Even restarts are possible. But this usually happens only under heavy loads or several simultaneous connections. Hopefully, this will be improved in iOS7.
The unfamous Unknown error started to appear for several developers recently. Judging from your description there are probably a number of reasons why your setup may fail and it would require much more info that what fits well into a SO question.
For more info I suggest you search the bluetooth-dev mailing list archives https://lists.apple.com/archives/Bluetooth-dev or send a mail Bluetooth-dev#lists.apple.com. The community provides great help if you approach with reasonable questions like this.
As per my understanding one device can work with one mode at a time . That is if the device is working in the peripheral mode then it you cant work it as a central mode .If you see some standard examples like BTLE transfer or lilke Light Blue those are working in one mode at a time .
Firstly, what do you mean "the same time"?
If you mean the device advertising to other devices while it scanning for other devices, it can not.
But you can create two threads which share same lock to advertising and scanning.
Before scanning, stop advertising, before advertising, stop scanning.
I tested on my iPhone 4s and iPad air, worked well.
I've got an iPhone, I've got a bluetooth low energy (aka Bluetooth 4.o) device. The two have connected in the past. But now they're apart. Something happened, words were said that couldn't be taken back, and now they've disconnected.
Now they're in the same room again. The BTLE device wants to connect again. It doesn't want to wait for the iPhone to call it. It's taking the initiative. It wants to talk to the iPhone, and get that app they used to share launched again.
How do I set up the iPhone code to support this?
(Wow, corny.)
You have to leave the app connecting to the device, so when the device becomes available the app will connect to it (so long as the app is alive). Just call [centralManager connectPeripheral] on the CMPeripheral, so the phone will keep listening for the device and connect immediately when it sees it. That doesn't time out, so you can just leave it running forever.
If no app is to connect to the peripheral, there's nothing the peripheral can do to change that.
This is a little late, but here's what I would do to accomplish this:
When your device wants to connect, have it advertise a special service.
Assuming the phone is already scanning for devices, it will see the service and you can auto-connect to it.
Now you can have the periphal trigger whatever you like on the central by updating a characteristic, etc.
Of course, this relies on the phone scanning at the time you need the action to trigger, but that is the nature of the beast.