| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- //
- // G7BluetoothManager.swift
- // CGMBLEKit
- //
- // Created by Pete Schwamb on 11/11/22.
- // Copyright © 2022 LoopKit Authors. All rights reserved.
- //
- import CoreBluetooth
- import Foundation
- import os.log
- enum PeripheralConnectionCommand {
- case connect
- case makeActive
- case ignore
- }
- protocol G7BluetoothManagerDelegate: AnyObject {
- /**
- Tells the delegate that the bluetooth manager has finished connecting to and discovering all required services of its peripheral
- - parameter manager: The bluetooth manager
- - parameter peripheralManager: The peripheral manager
- - parameter error: An error describing why bluetooth setup failed
- - returns: True if scanning should stop
- */
- func bluetoothManager(_ manager: G7BluetoothManager, readied peripheralManager: G7PeripheralManager) -> Bool
- /**
- Tells the delegate that the bluetooth manager encountered an error while connecting to and discovering required services of a peripheral
- - parameter manager: The bluetooth manager
- - parameter peripheralManager: The peripheral manager
- - parameter error: An error describing why bluetooth setup failed
- */
- func bluetoothManager(_ manager: G7BluetoothManager, readyingFailed peripheralManager: G7PeripheralManager, with error: Error)
- /**
- Asks the delegate if the discovered or restored peripheral is active or should be connected to
- - parameter manager: The bluetooth manager
- - parameter peripheral: The found peripheral
- - returns: PeripheralConnectionCommand indicating what should be done with this peripheral
- */
- func bluetoothManager(_ manager: G7BluetoothManager, shouldConnectPeripheral peripheral: CBPeripheral) -> PeripheralConnectionCommand
- /// Informs the delegate that the bluetooth manager received new data in the control characteristic
- ///
- /// - Parameters:
- /// - manager: The bluetooth manager
- /// - peripheralManager: The peripheral manager
- /// - response: The data received on the control characteristic
- func bluetoothManager(_ manager: G7BluetoothManager, peripheralManager: G7PeripheralManager, didReceiveControlResponse response: Data)
- /// Informs the delegate that the bluetooth manager received new data in the backfill characteristic
- ///
- /// - Parameters:
- /// - manager: The bluetooth manager
- /// - response: The data received on the backfill characteristic
- func bluetoothManager(_ manager: G7BluetoothManager, didReceiveBackfillResponse response: Data)
- /// Informs the delegate that the bluetooth manager received new data in the authentication characteristic
- ///
- /// - Parameters:
- /// - manager: The bluetooth manager
- /// - peripheralManager: The peripheral manager
- /// - response: The data received on the authentication characteristic
- func bluetoothManager(_ manager: G7BluetoothManager, peripheralManager: G7PeripheralManager, didReceiveAuthenticationResponse response: Data)
- /// Informs the delegate that the bluetooth manager started or stopped scanning
- ///
- /// - Parameters:
- /// - manager: The bluetooth manager
- func bluetoothManagerScanningStatusDidChange(_ manager: G7BluetoothManager)
- /// Informs the delegate that a peripheral disconnected
- ///
- /// - Parameters:
- /// - manager: The bluetooth manager
- func peripheralDidDisconnect(_ manager: G7BluetoothManager, peripheralManager: G7PeripheralManager, wasRemoteDisconnect: Bool)
- }
- class G7BluetoothManager: NSObject {
- weak var delegate: G7BluetoothManagerDelegate?
- private let log = OSLog(category: "G7BluetoothManager")
- /// Isolated to `managerQueue`
- private var centralManager: CBCentralManager! = nil
- /// Isolated to `managerQueue`
- private var activePeripheral: CBPeripheral? {
- get {
- return activePeripheralManager?.peripheral
- }
- }
- /// Isolated to `managerQueue`
- private var managedPeripherals: [UUID:G7PeripheralManager] = [:]
- var activePeripheralIdentifier: UUID? {
- get {
- return lockedPeripheralIdentifier.value
- }
- }
- private let lockedPeripheralIdentifier: Locked<UUID?> = Locked(nil)
- /// Isolated to `managerQueue`
- private var activePeripheralManager: G7PeripheralManager? {
- didSet {
- oldValue?.delegate = nil
- lockedPeripheralIdentifier.value = activePeripheralManager?.peripheral.identifier
- }
- }
- // MARK: - Synchronization
- private let managerQueue = DispatchQueue(label: "com.loudnate.CGMBLEKit.bluetoothManagerQueue", qos: .unspecified)
- override init() {
- super.init()
- managerQueue.sync {
- self.centralManager = CBCentralManager(delegate: self, queue: managerQueue, options: [CBCentralManagerOptionRestoreIdentifierKey: "com.loudnate.CGMBLEKit"])
- }
- }
- // MARK: - Actions
- func scanForPeripheral() {
- dispatchPrecondition(condition: .notOnQueue(managerQueue))
- managerQueue.sync {
- self.managerQueue_scanForPeripheral()
- }
- }
- func forgetPeripheral() {
- managerQueue.sync {
- self.activePeripheralManager = nil
- }
- }
- func stopScanning() {
- managerQueue.sync {
- managerQueue_stopScanning()
- }
- }
- private func managerQueue_stopScanning() {
- if centralManager.isScanning {
- log.debug("Stopping scan")
- centralManager.stopScan()
- delegate?.bluetoothManagerScanningStatusDidChange(self)
- }
- }
- func disconnect() {
- dispatchPrecondition(condition: .notOnQueue(managerQueue))
- managerQueue.sync {
- if centralManager.isScanning {
- log.debug("Stopping scan on disconnect")
- centralManager.stopScan()
- delegate?.bluetoothManagerScanningStatusDidChange(self)
- }
- if let peripheral = activePeripheral {
- centralManager.cancelPeripheralConnection(peripheral)
- }
- }
- }
- private func managerQueue_scanForPeripheral() {
- dispatchPrecondition(condition: .onQueue(managerQueue))
- guard centralManager.state == .poweredOn else {
- return
- }
- let currentState = activePeripheral?.state ?? .disconnected
- guard currentState != .connected else {
- return
- }
- if let peripheralID = activePeripheralIdentifier, let peripheral = centralManager.retrievePeripherals(withIdentifiers: [peripheralID]).first {
- log.debug("Retrieved peripheral %{public}@", peripheral.identifier.uuidString)
- handleDiscoveredPeripheral(peripheral)
- } else {
- for peripheral in centralManager.retrieveConnectedPeripherals(withServices: [
- SensorServiceUUID.advertisement.cbUUID,
- SensorServiceUUID.cgmService.cbUUID
- ]) {
- handleDiscoveredPeripheral(peripheral)
- }
- }
- if activePeripheral == nil {
- log.debug("Scanning for peripherals")
- centralManager.scanForPeripherals(withServices: [
- SensorServiceUUID.advertisement.cbUUID
- ],
- options: nil
- )
- delegate?.bluetoothManagerScanningStatusDidChange(self)
- }
- }
- /**
- Persistent connections don't seem to work with the transmitter shutoff: The OS won't re-wake the
- app unless it's scanning.
- The sleep gives the transmitter time to shut down, but keeps the app running.
- */
- fileprivate func scanAfterDelay() {
- DispatchQueue.global(qos: .utility).async {
- Thread.sleep(forTimeInterval: 2)
- self.scanForPeripheral()
- }
- }
- // MARK: - Accessors
- var isScanning: Bool {
- dispatchPrecondition(condition: .notOnQueue(managerQueue))
- var isScanning = false
- managerQueue.sync {
- isScanning = centralManager.isScanning
- }
- return isScanning
- }
- var isConnected: Bool {
- dispatchPrecondition(condition: .notOnQueue(managerQueue))
- var isConnected = false
- managerQueue.sync {
- isConnected = activePeripheral?.state == .connected
- }
- return isConnected
- }
- private func handleDiscoveredPeripheral(_ peripheral: CBPeripheral) {
- dispatchPrecondition(condition: .onQueue(managerQueue))
- if let delegate = delegate {
- switch delegate.bluetoothManager(self, shouldConnectPeripheral: peripheral) {
- case .makeActive:
- log.debug("Making peripheral active: %{public}@", peripheral.identifier.uuidString)
- if let peripheralManager = activePeripheralManager {
- peripheralManager.peripheral = peripheral
- } else {
- activePeripheralManager = G7PeripheralManager(
- peripheral: peripheral,
- configuration: .dexcomG7,
- centralManager: centralManager
- )
- activePeripheralManager?.delegate = self
- }
- self.managedPeripherals[peripheral.identifier] = activePeripheralManager
- self.centralManager.connect(peripheral)
- case .connect:
- log.debug("Connecting to peripheral: %{public}@", peripheral.identifier.uuidString)
- self.centralManager.connect(peripheral)
- let peripheralManager = G7PeripheralManager(
- peripheral: peripheral,
- configuration: .dexcomG7,
- centralManager: centralManager
- )
- peripheralManager.delegate = self
- self.managedPeripherals[peripheral.identifier] = peripheralManager
- case .ignore:
- break
- }
- }
- }
- override var debugDescription: String {
- return [
- "## BluetoothManager",
- activePeripheralManager.map(String.init(reflecting:)) ?? "No peripheral",
- ].joined(separator: "\n")
- }
- }
- extension G7BluetoothManager: CBCentralManagerDelegate {
- func centralManagerDidUpdateState(_ central: CBCentralManager) {
- dispatchPrecondition(condition: .onQueue(managerQueue))
- activePeripheralManager?.centralManagerDidUpdateState(central)
- log.default("%{public}@: %{public}@", #function, String(describing: central.state.rawValue))
- switch central.state {
- case .poweredOn:
- managerQueue_scanForPeripheral()
- case .resetting, .poweredOff, .unauthorized, .unknown, .unsupported:
- fallthrough
- @unknown default:
- if central.isScanning {
- log.debug("Stopping scan on central not powered on")
- central.stopScan()
- delegate?.bluetoothManagerScanningStatusDidChange(self)
- }
- }
- }
- func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
- dispatchPrecondition(condition: .onQueue(managerQueue))
- if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] {
- for peripheral in peripherals {
- log.default("Restoring peripheral from state: %{public}@", peripheral.identifier.uuidString)
- handleDiscoveredPeripheral(peripheral)
- }
- }
- }
- func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
- dispatchPrecondition(condition: .onQueue(managerQueue))
- log.info("%{public}@: %{public}@, data = %{public}@", #function, peripheral, String(describing: advertisementData))
- managerQueue.async {
- self.handleDiscoveredPeripheral(peripheral)
- }
- }
- func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
- dispatchPrecondition(condition: .onQueue(managerQueue))
- log.default("%{public}@: %{public}@", #function, peripheral)
- if let peripheralManager = managedPeripherals[peripheral.identifier] {
- peripheralManager.centralManager(central, didConnect: peripheral)
- if let delegate = delegate, case .poweredOn = centralManager.state, case .connected = peripheral.state {
- if delegate.bluetoothManager(self, readied: peripheralManager) {
- managerQueue_stopScanning()
- }
- }
- }
- }
- func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
- dispatchPrecondition(condition: .onQueue(managerQueue))
- log.default("%{public}@: %{public}@", #function, peripheral)
- // Ignore errors indicating the peripheral disconnected remotely, as that's expected behavior
- if let error = error as NSError?, CBError(_nsError: error).code != .peripheralDisconnected {
- log.error("%{public}@: %{public}@", #function, error)
- if let peripheralManager = activePeripheralManager {
- self.delegate?.bluetoothManager(self, readyingFailed: peripheralManager, with: error)
- }
- }
- if let peripheralManager = managedPeripherals[peripheral.identifier] {
- let remoteDisconnect: Bool
- if let error = error as NSError?, CBError(_nsError: error).code == .peripheralDisconnected {
- remoteDisconnect = true
- } else {
- remoteDisconnect = false
- }
- self.delegate?.peripheralDidDisconnect(self, peripheralManager: peripheralManager, wasRemoteDisconnect: remoteDisconnect)
- }
- if peripheral != activePeripheral {
- managedPeripherals.removeValue(forKey: peripheral.identifier)
- }
- scanAfterDelay()
- }
- func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
- dispatchPrecondition(condition: .onQueue(managerQueue))
- log.error("%{public}@: %{public}@", #function, String(describing: error))
- if let error = error, let peripheralManager = activePeripheralManager {
- self.delegate?.bluetoothManager(self, readyingFailed: peripheralManager, with: error)
- }
- if peripheral != activePeripheral {
- managedPeripherals.removeValue(forKey: peripheral.identifier)
- }
- scanAfterDelay()
- }
- }
- extension G7BluetoothManager: G7PeripheralManagerDelegate {
- func peripheralManager(_ manager: G7PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) {
- }
- func peripheralManagerDidUpdateName(_ manager: G7PeripheralManager) {
- }
- func peripheralManagerDidConnect(_ manager: G7PeripheralManager) {
- }
- func completeConfiguration(for manager: G7PeripheralManager) throws {
- }
- func peripheralManager(_ manager: G7PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) {
- guard let value = characteristic.value else {
- return
- }
- switch CGMServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString.uppercased()) {
- case .none, .communication?:
- return
- case .control?:
- self.delegate?.bluetoothManager(self, peripheralManager: manager, didReceiveControlResponse: value)
- case .backfill?:
- self.delegate?.bluetoothManager(self, didReceiveBackfillResponse: value)
- case .authentication?:
- self.delegate?.bluetoothManager(self, peripheralManager: manager, didReceiveAuthenticationResponse: value)
- }
- }
- }
|