Upgrade to Pro — share decks privately, control downloads, hide ads and more …

When iPhones and Wearables Dance the Bluetooth Dance

When iPhones and Wearables Dance the Bluetooth Dance

Creating a mobile app that communicates with a custom wearable device via a Bluetooth Low Energy connection seems like an unusual experience, that wouldn't possibly happen to you, would it? Well, let me tell you that the Internet of Things is here and it's full of interesting opportunities, so we, as a community, should keep learning and get ready to seize them!

This talk was given at United Dev Conf 2017 (http://unitedconf.com) and includes stories and lessons learned about creating iOS apps and SDKs to interface with smartwatches made for monitoring physiological signals.

Alberto Guarino

April 06, 2017
Tweet

More Decks by Alberto Guarino

Other Decks in Programming

Transcript

  1. TODAY'S ADVENTURE > Wearables & My Story > Coding with

    BLE on iOS > The importance of protocols > Connection stability > Testing with wearables > Logging & monitoring > Remote maintenance > The End
  2. TODAY A WEARABLE WILL BE > A piece of HW

    that sits somewhere on your body > Easy to carry with you > Provides useful data via sensors > Uses BLE for data exchange with the outside world > In need of an app to communicate with > Custom: you know / have power on the internals
  3. CORE BLUETOOTH The framework is an abstraction of the Bluetooth

    4.0 specification for use with low energy devices. That said, it hides many of the low-level details of the specification from you, the developer, making it much easier for you to develop apps that interact with Bluetooth low energy devices. — Apple Docs
  4. PERIPHERAL > It exposes services > Services contain characteristics >

    Characteristics allow data exchange > You can define your own
  5. CENTRAL > Wants data > Scans for advertising peripherals >

    Connects to peripherals > Explores services and characteristics > Reads, writes, is notified by characteristics > It's (part of) our app > CBCentralManager
  6. SCANNING let central = CBCentralManager(delegate: self, queue: nil) let serviceUUID

    = CBUUID(string: "ABC00001-A0B1-FFFF-1234-1A2B3DD11001") // Set the delegate (someDelegate: CBCentralManagerDelegate) central.delegate = someDelegate // Start Scanning central.scanForPeripherals(withServices: [serviceUUID], options: nil)
  7. DISCOVERY var peripheral: CBPeripheral? // CBCentralManagerDelegate discovery handler func centralManager(_

    central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { if thisIsMy(peripheral) { // Check before connecting central.stopScan() // Please do! self.peripheral = peripheral // Keep a strong ref! // Step 3 here } }
  8. FAILURE HANDLER // CBCentralManagerDelegate method func centralManager(_ central: CBCentralManager, didFailToConnect

    peripheral: CBPeripheral, error: Error?) { // Recovery // Notify users // ... // Give up? ! }
  9. UNENCRYPTED CHARACTERISTICS No BLE encryption on the device side >

    You can interface with the device immediately > It will go back to advertising mode when disconnected from the app > It will need a new scan to be found (or a direct connection via UUID)
  10. ENCRYPTED CHARACTERISTICS > The device reconnects automatically to iOS >

    You'll need to retrieve it from known peripherals: let peripherals = central .retrieveConnectedPeripherals(withServices: [serviceUUID])
  11. ENCRYPTED CHARACTERISTICS The pairing can be "delicate": > You can't

    retrieve the device anymore > You can't connect to the retrieved device > You can't exchange data with the device You can't solve this with code: > Soft solution: kill your app, forget device > Hard solution: restart your phone
  12. AFTER CONNECTING peripheral.discoverServices([serviceUUID]) peripheral.discoverCharacteristics([charUUID], for: service) peripheral.setNotifyValue(true, for: characteristic) //

    In your CBPeripheralDelegate func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if let data = characteristic.value { // data: Data // Enjoy your data! ! } }
  13. THOU SHALT HAVE A SHARED PROTOCOL > Available services and

    characteristics > Commands and command format > Response format (how to parse bytes)
  14. PROTOCOL EXAMPLE // Command Characteristic let charUUID = CBUUID(string: "ABC00001-A0B1-FFFF-1234-1A2B3DD11002")

    let char: CBCharacteristic // We have already discovered this // Available Commands enum CommandId: UInt8 { case test // 0 case readTemp // 1 case writeTemp // 2 } // Issuing the Read Temperature command let readTempData = Data(bytes: [CommandId.readTemp.rawValue]) peripheral.writeValue(readTempData, for: char, type: .withResponse)
  15. PARSING THE RESPONSE // Inside didUpdateValueForCharacteristic handler const uint8_t *data

    = (uint8_t *)[characteristic.value bytes]; uint8_t packetID = data[0]; if (packetID == TEMP_RESPONSE_ID) { // ID for the temperature response packet uint32_t timestamp; memcpy(&timestamp, &data[1], 4); int8_t temperature; memcpy(&temperature, &data[5], 1); // Work with temp value & timestamp }
  16. MESSING WITH THE PROTOCOL? // V1 enum CommandId: UInt8 {

    case test // 0 case readTemp // 1 case writeTemp // 2 } // V2 enum CommandId: UInt8 { case test // 0 case reset // 1 <=== NEW COMMAND HERE! ! case readTemp // 2 case writeTemp // 3 }
  17. RECAP > We know how to discover our custom wearable

    > We know how to connect and pair it with iOS > We know how to subscribe to its characteristics > We know how to parse device data > WE KNOW HOW TO DANCE! !"
  18. CONNECTION STABILITY > Your wearable device measures critical data >

    It needs to be always connected > The background is a completely different story: For iOS apps, it is crucial to know whether your app is running in the foreground or the background. An app must behave differently in the background than in the foreground, because system resources are more limited on iOS devices
  19. STATE PRESERVATION AND RESTORATION let queue = DispatchQueue(label: "mySerialDispatchQueue") let

    options = [CBCentralManagerOptionRestoreIdentifierKey: "myCentralManagerIdentifier"] let centralManager = CBCentralManager(delegate: self, queue: queue, options: options)
  20. AS A RESULT... > Your app will continue working in

    the background > If your app is suspended, the system will track: > the services it was scanning for > the peripherals it was connected / connecting to > the characteristics it was subscribed to > Related BLE events will make the system relaunch your app in the background
  21. ERROR #1 ASSUMING APP RELAUNCH WILL ALWAYS WORK If the

    users kills your app, there's no coming back from it SOLUTION ✅ Let your users know
  22. ERROR #2 ASSUMING THE BACKGROUND WILL JUST WORK If the

    BLE connection is dropped in the background and the app gets suspended, it won't be relaunched SOLUTION ✅ Try to reconnect immediately ✅ If you need to do some processing, start a background task
  23. ERROR #3 ASSUMING YOU HAVE INFINITE TIME "10 second" rule:

    each BLE event renews your background time for about 10 seconds. SOLUTION ✅ Have your wearable ping your app more often than every 10 seconds ✅ Start a background task if you miss a packet
  24. ERROR #4 TRUSTING IOS CHANGELOGS iOS updates break the Bluetooth

    in subtle ways SOLUTION ✅ TEST, TEST, TEST each iOS release
  25. BASIC TESTING > Write your unit tests > Do your

    TDD > Automate your tests > Maybe add some UI testing > Use continuous integration > Involve your QA engineer colleagues
  26. ...WITH WEARABLES? > Focus on full system integration > Involve

    all the components > Track exactly what you're testing: HW revision, FW version, OS, ... > Have clearly defined test scenarios and store test reports > Sadly, the simulator won't work here
  27. YOU'RE NOT ALONE IN THIS Make sure these people are

    your best friends: > FW developer: custom FW, insight about the device... > QA engineer: test scenarios, collecting reports... > Your colleagues: will run around with you !
  28. SAMPLE TEST SCENARIO > Custom FW that simulates disconnections >

    Move around the room, app in foreground, see what happens > Send the app to the background, see if it dies on you or if it recovers the connection > Check what happened (HW + SW) > Repeat this on every FW / App / OS update!
  29. LOGGING AND MONITORING > NEVER forget about logging > Remote

    logging (in the cloud) > Via the app (no internet access on device) > Session reconstruction from start to finish
  30. REMOTE COMMANDS // Note: this is only a partial implementation

    enum RemoteCommandId: Int { case ping = 0 case checkStatus = 1 case resetDevice = 2 // ... }
  31. REMOTE COMMANDS struct RemoteCommand { // ... static func parseRemoteCommand(userInfo:

    [AnyHashable : Any]) -> (Bool, RemoteCommand?) { if let cmdIdInt = userInfo["command"] as? Int, let cmdId = RemoteCommandId(rawValue: cmdIdInt) { let delay = (userInfo["delay"] as? Int) ?? 0 let params = userInfo["parameters"] as? [String: Any] let cmd = RemoteCommand(id: cmdId, delay: delay, params: params) return (true, cmd) } return (false, nil) }
  32. SILENT NOTIFICATIONS { "aps" : { "content-available" : 1 },

    "command" : 1, // Check Status "delay" : 0 // Now, "parameters": [ // Your custom parameters ] }
  33. RECEIVING AND PARSING let executor = RemoteExecutor() func application(_ application:

    UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { // Check if this is a remote command let (isCommand, command) = RemoteCommand.parseRemoteCommand(userInfo: userInfo) if isCommand { executor.run(command: command) } completionHandler(.noData) }
  34. EXECUTING // Note: this is only a partial implementation class

    RemoteExecutor { let peripheral: CBPeripheral let char: CBCharacteristic func run(command: RemoteCommand) { switch command.id { case .checkStatus: // Turn command into Data let cmdData = Data(bytes: [CommandId.checkStatus.rawValue]) peripheral.writeValue(cmdData, for: char, type: .withResponse) default: break } } }
  35. RECAP > Know your BLE basics > Use encrypted chars,

    but know that the iOS BLE stack can get stuck > Don't be afraid to resort to ObjC > Keep your connection stable, especially in the background > Always test at the system-integration level > Don't trust iOS updates > Flood the cloud with logs > Implement remote maintenance