USB-Meter / USB Meter / Model / BluetoothManager.swift
Newer Older
176 lines | 7.686kb
Bogdan Timofte authored 2 weeks ago
1
//
2
//  BTManager.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 01/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8

            
9
import CoreBluetooth
10

            
11
class BluetoothManager : NSObject, ObservableObject {
12
    private var manager: CBCentralManager!
13
    // MARK: MacOS split advertisementData generating multiple discoveries with partial data: https://stackoverflow.com/questions/41628114/cbperipheral-advertisementdata-is-different-when-discovering-peripherals-on-osx.
14
    #if targetEnvironment(macCatalyst)
15
    private var advertisementDataCache = AdvertisementDataCache()
16
    #endif
17
    @Published var managerState = CBManagerState.unknown
18

            
19
    override init () {
20
        super.init()
21
        manager = CBCentralManager(delegate: self, queue: nil)
22
    }
23

            
24

            
25
    private func scanForMeters() {
26
        guard manager.state == .poweredOn else {
27
            track( "Scan requested but Bluetooth state is \(manager.state)")
28
            return
29
        }
30
        //manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ])
31
        manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ])
32
    }
33

            
34
    func discoveredMeter(peripheral: CBPeripheral, advertising advertismentData: [String : Any], rssi RSSI: NSNumber) {
35
        //track("discovered new USB Meter: (\(peripheral), advertsing \(advertismentData)")
36
        if let peripheralName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines) {
37
            if let kCBAdvDataManufacturerData = advertismentData["kCBAdvDataManufacturerData"] as? Data {
38
                // MARK: MAC Address
39
                let macAddress = MACAddress(from: kCBAdvDataManufacturerData.suffix(from: 2))
40
                // MARK: Model
41
                if let model = ModelByPeriferalName[peripheralName] {
42
                    //track("Tetermided model for peripheral name: '\(peripheralName)'")
43
                    // MARK: Known Meters Lookup
44
                    if appData.meters[peripheral.identifier] == nil {
45
                        track("adding new USB Meter named '\(peripheralName)' with MAC Address: '\(macAddress)'")
46
                        let btSerial = BluetoothSerial(peripheral: peripheral, radio:  modelRadios[model] ?? .UNKNOWN, with: macAddress, managedBy: manager, RSSI: RSSI.intValue)
47
                        var m = appData.meters
48
                        m[peripheral.identifier] = Meter(model: model, with: btSerial)
49
                        appData.meters = m
50
                    } else {
51
//                        track("Updating USB Meter: \(peripheral.identifier) ")
52
                        peripheral.delegate?.peripheral?(peripheral, didReadRSSI: RSSI, error: nil)
53
                    }
54
                } else {
55
                    track("Unable to determine model for peripheral name: '\(peripheralName)'")
56
                }
57
            } else {
58
                track("Insuficient data to use device!")
59
            }
60
        }
61
        else{
62
            track("Periferal: \(peripheral.identifier) does not have a name")
63
        }
64
    }
65
}
66

            
67
extension BluetoothManager : CBCentralManagerDelegate {
68
    // MARK:  CBCentralManager state Changed
69
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
70
        managerState = central.state;
71
        track("\(central.state)")
72

            
73
        switch central.state {
74
        case .poweredOff:
75
            track("Bluetooth is Off. How should I behave?")
76
        case .poweredOn:
77
            track("Bluetooth is On... Start scanning...")
78
            // note that "didDisconnectPeripheral" won't be called if BLE is turned off while connected
79
            // connectedPeripheral = nil
80
            // pendingPeripheral = nil
81
            DispatchQueue.global(qos: .userInitiated).async { [weak self] in
82
                self?.scanForMeters()
83
            }
84
        case .resetting:
85
            track("Bluetooth is reseting... . Whatever that means.")
86
        case .unauthorized:
87
            track("Bluetooth is not authorized.")
88
        case .unknown:
89
            track("Bluetooth is in an unknown state.")
90
        case .unsupported:
91
            track("Bluetooth not supported by device")
92
        default:
93
            track("Bluetooth is in a state never seen before!")
94
        }
95
    }
96

            
97
    // MARK: MacOS multiple discoveries advertisementData caching
98
    #if targetEnvironment(macCatalyst)
99
    private class AdvertisementDataCache {
100

            
101
        fileprivate var map = [UUID: [String: Any]]()
102

            
103
        func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
104
            var ad = (map[peripheral.identifier]) ?? [String: Any]()
105
            for (key, value) in advertisementData {
106
                ad[key] = value
107
            }
108
            map[peripheral.identifier] = ad
109
            return ad
110
        }
111

            
112
        func clear() {
113
            map.removeAll()
114
        }
115
    }
116
    #endif
117

            
118
    // MARK:  CBCentralManager didDiscover peripheral
119
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
120
        #if targetEnvironment(macCatalyst)
121
// MARK: MacOS probably assumes that if "kCBAdvDataIsConnectable" is not present in parial advertisment data it nust be 0
122
//        var ad = advertisementData
123
//        if ( ad["kCBAdvDataManufacturerData"] == nil ) {
124
//            ad.removeValue(forKey: "kCBAdvDataIsConnectable")
125
//        }
126
        let completeAdvertisementData = self.advertisementDataCache.append(peripheral: peripheral, advertisementData: advertisementData)
127
        #else
128
        let completeAdvertisementData = advertisementData
129
        #endif
130
        //track("Device discoverded UUID: '\(peripheral.identifier)' named '\(peripheral.name ?? "Unknown")'); RSSI: \(RSSI) dBm; Advertisment data: \(advertisementData)")
131
        discoveredMeter(peripheral: peripheral, advertising: completeAdvertisementData, rssi: RSSI )
132
    }
133

            
134
    // MARK:  CBCentralManager didConnect peripheral
135
    internal func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
136
        //track("Connected to peripheral: '\(peripheral.identifier)'")
137
        if let usbMeter = appData.meters[peripheral.identifier] {
138
            usbMeter.btSerial.connectionEstablished()
139
        }
140
        else {
141
            track("Connected to unknown meter with UUID: '\(peripheral.identifier)'")
142
        }
143
    }
144

            
145
    // MARK:  CBCentralManager didDisconnectPeripheral peripheral
146
    internal func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
147
        track("Disconnected from peripheral: '\(peripheral.identifier)' with error: \(error.debugDescription)")
148
        if let usbMeter = appData.meters[peripheral.identifier] {
149
            usbMeter.btSerial.connectionClosed()
150
        }
151
        else {
152
            track("Disconnected from unknown meter with UUID: '\(peripheral.identifier)'")
153
        }
154
    }
155
}
156

            
157
// MARK: MacOS multiple discoveries advertisementData caching
158
#if targetEnvironment(macCatalyst)
159
private class AdvertisementDataCache {
160

            
161
    fileprivate var map = [UUID: [String: Any]]()
162

            
163
    func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
164
        var ad = (map[peripheral.identifier]) ?? [String: Any]()
165
        for (key, value) in advertisementData {
166
            ad[key] = value
167
        }
168
        map[peripheral.identifier] = ad
169
        return ad
170
    }
171

            
172
    func clear() {
173
        map.removeAll()
174
    }
175
}
176
#endif