// // 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 advertisementDataCache = AdvertisementDataCache() @Published var managerState = CBManagerState.unknown override init () { super.init() } func start() { guard manager == nil else { return } track("Starting Bluetooth manager and requesting authorization if needed") 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 = ModelByPeriferalName[peripheralName] else { return } let macAddress = MACAddress(from: manufacturerData.suffix(from: 2)) if appData.meters[peripheral.identifier] == nil { track("adding new USB Meter named '\(peripheralName)' with MAC Address: '\(macAddress)'") let btSerial = BluetoothSerial(peripheral: peripheral, radio: modelRadios[model] ?? .UNKNOWN, 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.RSSI = RSSI.intValue 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: track("Bluetooth is Off. How should I behave?") case .poweredOn: track("Bluetooth is On... Start scanning...") // note that "didDisconnectPeripheral" won't be called if BLE is turned off while connected // connectedPeripheral = nil // pendingPeripheral = nil DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.scanForMeters() } case .resetting: track("Bluetooth is reseting... . Whatever that means.") case .unauthorized: track("Bluetooth is not authorized.") case .unknown: track("Bluetooth is in an unknown state.") case .unsupported: track("Bluetooth not supported by device") default: 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 unknown 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 unknown 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() } }