// // BTManager.swift // USB Meter // // Created by Bogdan Timofte on 01/03/2020. // Copyright © 2020 Bogdan Timofte. All rights reserved. // import CoreBluetooth class BluetoothManager : NSObject, ObservableObject { private var manager: CBCentralManager? private var isStarting = false private var advertisementDataCache = AdvertisementDataCache() @Published var managerState = CBManagerState.unknown @Published private(set) var scanStartedAt: Date? override init () { super.init() } func start() { guard manager == nil, !isStarting else { return } isStarting = true track("Starting Bluetooth manager and requesting authorization if needed") DispatchQueue.main.async { [weak self] in guard let self else { return } defer { self.isStarting = false } guard self.manager == nil else { return } self.manager = CBCentralManager(delegate: self, queue: nil) } } private func scanForMeters() { guard let manager else { track("Scan requested before Bluetooth manager was started") return } guard manager.state == .poweredOn else { track( "Scan requested but Bluetooth state is \(manager.state)") return } //manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ]) manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ]) } func discoveredMeter(peripheral: CBPeripheral, advertising advertismentData: [String : Any], rssi RSSI: NSNumber) { guard let peripheralName = resolvedPeripheralName(for: peripheral, advertising: advertismentData) else { return } guard let manufacturerData = resolvedManufacturerData(from: advertismentData), manufacturerData.count > 2 else { return } guard let model = Model.byPeripheralName[peripheralName] else { return } let macAddress = MACAddress(from: manufacturerData.suffix(from: 2)) let macAddressString = macAddress.description appData.registerMeter(macAddress: macAddressString, modelName: model.canonicalName, advertisedName: peripheralName) appData.noteMeterSeen(at: Date(), macAddress: macAddressString) if appData.meters[peripheral.identifier] == nil { track("adding new USB Meter named '\(peripheralName)' with MAC Address: '\(macAddress)'") let btSerial = BluetoothSerial(peripheral: peripheral, radio: model.radio, with: macAddress, managedBy: manager!, RSSI: RSSI.intValue) var m = appData.meters m[peripheral.identifier] = Meter(model: model, with: btSerial) appData.meters = m } else if let meter = appData.meters[peripheral.identifier] { meter.lastSeen = Date() meter.btSerial.updateRSSI(RSSI.intValue) let macAddress = meter.btSerial.macAddress.description if meter.name == macAddress, let syncedName = appData.meterName(for: macAddress), syncedName != macAddress { meter.updateNameFromStore(syncedName) } if peripheral.delegate == nil { peripheral.delegate = meter.btSerial } } } private func resolvedPeripheralName(for peripheral: CBPeripheral, advertising advertismentData: [String : Any]) -> String? { let candidates = [ (advertismentData[CBAdvertisementDataLocalNameKey] as? String), peripheral.name ] for candidate in candidates { if let trimmed = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty { return trimmed } } return nil } private func resolvedManufacturerData(from advertismentData: [String : Any]) -> Data? { if let data = advertismentData[CBAdvertisementDataManufacturerDataKey] as? Data { return data } if let data = advertismentData["kCBAdvDataManufacturerData"] as? Data { return data } return nil } } extension BluetoothManager : CBCentralManagerDelegate { // MARK: CBCentralManager state Changed func centralManagerDidUpdateState(_ central: CBCentralManager) { managerState = central.state; track("\(central.state)") switch central.state { case .poweredOff: scanStartedAt = nil track("Bluetooth is Off. How should I behave?") case .poweredOn: scanStartedAt = Date() track("Bluetooth is On... Start scanning...") // note that "didDisconnectPeripheral" won't be called if BLE is turned off while connected // connectedPeripheral = nil // pendingPeripheral = nil scanForMeters() case .resetting: scanStartedAt = nil track("Bluetooth is reseting... . Whatever that means.") case .unauthorized: scanStartedAt = nil track("Bluetooth is not authorized.") case .unknown: scanStartedAt = nil track("Bluetooth is in an unknown state.") case .unsupported: scanStartedAt = nil track("Bluetooth not supported by device") default: scanStartedAt = nil track("Bluetooth is in a state never seen before!") } } // MARK: CBCentralManager didDiscover peripheral func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { let completeAdvertisementData = self.advertisementDataCache.append(peripheral: peripheral, advertisementData: advertisementData) //track("Device discoverded UUID: '\(peripheral.identifier)' named '\(peripheral.name ?? "Unknown")'); RSSI: \(RSSI) dBm; Advertisment data: \(advertisementData)") discoveredMeter(peripheral: peripheral, advertising: completeAdvertisementData, rssi: RSSI ) } // MARK: CBCentralManager didConnect peripheral internal func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { //track("Connected to peripheral: '\(peripheral.identifier)'") if let usbMeter = appData.meters[peripheral.identifier] { usbMeter.btSerial.connectionEstablished() } else { track("Connected to meter with UUID: '\(peripheral.identifier)'") } } // MARK: CBCentralManager didDisconnectPeripheral peripheral internal func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { track("Disconnected from peripheral: '\(peripheral.identifier)' with error: \(error.debugDescription)") if let usbMeter = appData.meters[peripheral.identifier] { usbMeter.btSerial.connectionClosed() } else { track("Disconnected from meter with UUID: '\(peripheral.identifier)'") } } internal func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { track("Failed to connect to peripheral: '\(peripheral.identifier)' with error: \(error.debugDescription)") if let usbMeter = appData.meters[peripheral.identifier] { usbMeter.btSerial.connectionClosed() } else { track("Failed to connect to meter with UUID: '\(peripheral.identifier)'") } } } private class AdvertisementDataCache { private var map = [UUID: [String: Any]]() func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] { var ad = (map[peripheral.identifier]) ?? [String: Any]() if let localName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines), !localName.isEmpty { ad[CBAdvertisementDataLocalNameKey] = localName } for (key, value) in advertisementData { ad[key] = value } map[peripheral.identifier] = ad return ad } func clear() { map.removeAll() } }