Here at Five Pack, I’ve been working on a project that uses a customized Tertium Blueberry, an RFID reader that sends the data to a BTLE compatible device such as an iPhone. It’s a great little piece of hardware, but it’s been a challenge to get the project requirements working… or at least working smoothly and reliably. The challenge isn’t with the device, rather it’s with the iOS Bluetooth SDK.
While the various SDKs that Apple provides to developers are generally well thought out and easy to use, they also tend to hide some capabilities of iOS itself, often preventing a developer who needs some lower-level functionality from building a straightforward solution. Such is the case with CoreBluetooth.
Presented here are 3 things you should know (and account for) when coding for CoreBluetooth.
1. Device Discovery Is A One-Way Street
When you connect to the Bluetooth Central Manager (CM) and start scanning for a device that has particular services, the CM will let you know when it finds a device.
func centralManager( central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber)
However, there’s no corresponding “didUndiscoverPeripheral” type of function. In the case of my app, I needed to show an instruction screen, detect when the device was turned on (discovered), show a list of discovered devices, and let the user select it. If the device timed out or was turned off, I needed to go back to the instruction screen.
But… there’s no way to directly know if the device was turned off. So the trick to making Discovery a two-way street is this:
- Scan using CBCentralManagerScanOptionAllowDuplicatesKey = true and timeout when didDiscoverPeripheral no longer sees the device
In my app, I keep an array of discovered peripherals, with an extension on the CBPeripheral object to add a “refreshDate” property, indicating when the peripheral was last “seen” by the CM. Then a timer checks that array to see if the refreshDate has expired beyond a timeout factor.
extension CBPeripheral { private struct AssociatedKeys { static var refreshDate = "refreshDate" } // This updates as the CentralManager issues discovered events on the peripheral. It's used to timeout // the peripheral and remove it from the list of discovered peripherals since there aren't any events // fired when the scanner is turned off. var refreshDate : NSDate { get { guard let t = objc_getAssociatedObject(self,&AssociatedKeys.refreshDate) as? NSDate else { return NSDate() } return t } set(value) { objc_setAssociatedObject(self, &AssociatedKeys.refreshDate, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } }
The refreshDate is set in the didDiscoverPeripheral event. So that’s the first thing to know — make discovering peripherals a two way street so that you know both when it’s discovered, and when it’s undiscovered.
2. didConnectPeripheral Doesn’t Mean What You Think It Means!
Realizing this fact was a big Ah ha! moment. Even when the device is not paired, you’ll get a didConnectPeripheral event as soon as you call CM.connectPeripheral() and iOS is able to either truly connect and make the device available OR attempt to pair. Therefore you can’t just tell your app, “Hey, the device is available!” It might be… and it might not be. And, there aren’t any new events that occur when the device is actually paired… you just don’t know!
What I did was generate an instance of the device, mark it as “connectedAndUnpaired“, then attempt to start using it. In the case of the Blueberry, it has a way to check if the beep sound is on or off. However, you can only read specific details about a device if it is actually paired. Therefore, if I was able to read to sound state, I’d know to change it from “connectedAndUnpaired” to “connectedAndPaired“.
BTW, the Swift Optional is great for this, since soundOn : Bool? == nil indicates it’s still unpaired, while soundOn == (true | false) indicates it’s paired. So the second thing is realizing that a didConnectPeripheral event doesn’t mean that the peripheral is available for your app to use.
And… you’re still not out of the woods, because there’s one more quirk that you need to handle:
3. Discovery And Connect/Disconnect Are Exclusive
In other words, a device can’t be in both a state of discovery and a connected state. This means that the intermediate state I describe above as “connectedAndUnpaired” means that you can’t get rid of your “discovered” devices yet! In the app example, I stated that the List of powered devices needs to show until the device is powered off or successfully paired. But since Connect and Discovery are exclusive, it means that didDiscoverPeripheral is no longer firing and updating the refreshDate. However, I don’t want to remove it from the discover list until I’m sure it’s paired. So in the Timer function where I’m checking the refreshDate against the timeout interval, I have to make sure that a “timed out” peripheral isn’t in the “discoveredAndUnpaired” list. If it is, then I ignore the timeout.
When the didDisconnectPeripheral or didFailToConnectPeripheral fire, I remove the device from both the connectedAndUnpaired and the connectedAndPaired lists, and I check to see if it’s still in the discovered list and refresh the date (to prevent it from being removed by the timer until the CM has a chance to start firing the discover event again). So the third thing to know is that the didDisconnectPeripheral and didfailToConnectPeripheral events don’t necessarily mean that the peripheral is no longer around; that it powered off, rather it just means that there was a connection cancellation or problem, but the device might still be powered on and discovered.
The sequence of events is this:
- didDiscover
- Add to Discovered list if it isn’t already there
- Update the refreshDate
- Use a timer to check if the refreshDate is past an arbitrary timeout interval (I use 2 seconds)
- didConnect
- Create a device instance and add it to connectedAndUnpaired list
- Attempt to communicate with the device (sending a sound read command)
- Ignore this device’s peripheral in the timer
- Check the device’s property
- The device manager is a delegate for the devices
- The device fires a delegate.propertiesChanged event when it successfully reads the sound
- In the propertiesChanged event, if the sound is not nil, move the device from connectedAndUnpaired to connectedAndPaired, remove it from Discovered, and fire the delegate event for a successful pairing (in our case, it’s a device list change event).
- didDisconnect or didFailToConnect
- Remove from connectedAndUnpaired, remove from connectedAndPaired.
- If the peripheral exists in Discovered, refresh its date.
- Timeout of the refreshDate
- If not in the connectedAndUnpaired list, remove it from the Discovered list and fire the event that indicates the Discovered list has changed — useful for an “undiscovered” type of event.
I hope this helps you with your iOS Bluetooth coding!