USB-Meter / USB Meter / Model / BluetoothManager.swift
1 contributor
176 lines | 7.686kb
//
//  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