// // 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! // MARK: MacOS split advertisementData generating multiple discoveries with partial data: https://stackoverflow.com/questions/41628114/cbperipheral-advertisementdata-is-different-when-discovering-peripherals-on-osx. #if targetEnvironment(macCatalyst) private var advertisementDataCache = AdvertisementDataCache() #endif @Published var managerState = CBManagerState.unknown override init () { super.init() manager = CBCentralManager(delegate: self, queue: nil) } private func scanForMeters() { 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) { //track("discovered new USB Meter: (\(peripheral), advertsing \(advertismentData)") if let peripheralName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines) { if let kCBAdvDataManufacturerData = advertismentData["kCBAdvDataManufacturerData"] as? Data { // MARK: MAC Address let macAddress = MACAddress(from: kCBAdvDataManufacturerData.suffix(from: 2)) // MARK: Model if let model = ModelByPeriferalName[peripheralName] { //track("Tetermided model for peripheral name: '\(peripheralName)'") // MARK: Known Meters Lookup 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 { // track("Updating USB Meter: \(peripheral.identifier) ") peripheral.delegate?.peripheral?(peripheral, didReadRSSI: RSSI, error: nil) } } else { track("Unable to determine model for peripheral name: '\(peripheralName)'") } } else { track("Insuficient data to use device!") } } else{ track("Periferal: \(peripheral.identifier) does not have a name") } } } 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: MacOS multiple discoveries advertisementData caching #if targetEnvironment(macCatalyst) private class AdvertisementDataCache { fileprivate var map = [UUID: [String: Any]]() func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] { var ad = (map[peripheral.identifier]) ?? [String: Any]() for (key, value) in advertisementData { ad[key] = value } map[peripheral.identifier] = ad return ad } func clear() { map.removeAll() } } #endif // MARK: CBCentralManager didDiscover peripheral func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { #if targetEnvironment(macCatalyst) // MARK: MacOS probably assumes that if "kCBAdvDataIsConnectable" is not present in parial advertisment data it nust be 0 // var ad = advertisementData // if ( ad["kCBAdvDataManufacturerData"] == nil ) { // ad.removeValue(forKey: "kCBAdvDataIsConnectable") // } let completeAdvertisementData = self.advertisementDataCache.append(peripheral: peripheral, advertisementData: advertisementData) #else let completeAdvertisementData = advertisementData #endif //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)'") } } } // MARK: MacOS multiple discoveries advertisementData caching #if targetEnvironment(macCatalyst) private class AdvertisementDataCache { fileprivate var map = [UUID: [String: Any]]() func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] { var ad = (map[peripheral.identifier]) ?? [String: Any]() for (key, value) in advertisementData { ad[key] = value } map[peripheral.identifier] = ad return ad } func clear() { map.removeAll() } } #endif