USB-Meter / USB Meter / Model / BluetoothManager.swift
Newer Older
218 lines | 9.05kb
Bogdan Timofte authored 2 months 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 {
Bogdan Timofte authored 2 months ago
12
    private var manager: CBCentralManager?
Bogdan Timofte authored 2 months ago
13
    private var isStarting = false
Bogdan Timofte authored 2 months ago
14
    private var advertisementDataCache = AdvertisementDataCache()
15
    @Published var managerState = CBManagerState.unknown
Bogdan Timofte authored 2 months ago
16
    @Published private(set) var scanStartedAt: Date?
Bogdan Timofte authored 2 months ago
17

            
18
    override init () {
19
        super.init()
20
    }
21

            
Bogdan Timofte authored 2 months ago
22
    func start() {
Bogdan Timofte authored 2 months ago
23
        guard manager == nil, !isStarting else {
Bogdan Timofte authored 2 months ago
24
            return
25
        }
Bogdan Timofte authored 2 months ago
26
        isStarting = true
Bogdan Timofte authored 2 months ago
27
        track("Starting Bluetooth manager and requesting authorization if needed")
Bogdan Timofte authored 2 months ago
28
        DispatchQueue.main.async { [weak self] in
29
            guard let self else { return }
30
            defer { self.isStarting = false }
31
            guard self.manager == nil else {
32
                return
33
            }
34
            self.manager = CBCentralManager(delegate: self, queue: nil)
35
        }
Bogdan Timofte authored 2 months ago
36
    }
Bogdan Timofte authored 2 months ago
37

            
38
    private func scanForMeters() {
Bogdan Timofte authored 2 months ago
39
        guard let manager else {
40
            track("Scan requested before Bluetooth manager was started")
41
            return
42
        }
Bogdan Timofte authored 2 months ago
43
        guard manager.state == .poweredOn else {
44
            track( "Scan requested but Bluetooth state is \(manager.state)")
45
            return
46
        }
47
        //manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ])
48
        manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ])
49
    }
50

            
51
    func discoveredMeter(peripheral: CBPeripheral, advertising advertismentData: [String : Any], rssi RSSI: NSNumber) {
Bogdan Timofte authored 2 months ago
52
        guard let peripheralName = resolvedPeripheralName(for: peripheral, advertising: advertismentData) else {
53
            return
54
        }
55
        guard let manufacturerData = resolvedManufacturerData(from: advertismentData), manufacturerData.count > 2 else {
56
            return
57
        }
58

            
Bogdan Timofte authored 2 months ago
59
        guard let model = Model.byPeripheralName[peripheralName] else {
Bogdan Timofte authored 2 months ago
60
            return
61
        }
62

            
63
        let macAddress = MACAddress(from: manufacturerData.suffix(from: 2))
Bogdan Timofte authored 2 months ago
64
        let macAddressString = macAddress.description
Bogdan Timofte authored 2 months ago
65
        appData.registerMeter(macAddress: macAddressString, modelName: model.canonicalName, advertisedName: peripheralName)
Bogdan Timofte authored 2 months ago
66
        appData.noteMeterSeen(at: Date(), macAddress: macAddressString)
Bogdan Timofte authored 2 months ago
67

            
68
        if appData.meters[peripheral.identifier] == nil {
69
            track("adding new USB Meter named '\(peripheralName)' with MAC Address: '\(macAddress)'")
Bogdan Timofte authored 2 months ago
70
            let btSerial = BluetoothSerial(peripheral: peripheral, radio: model.radio, with: macAddress, managedBy: manager!, RSSI: RSSI.intValue)
Bogdan Timofte authored 2 months ago
71
            var m = appData.meters
Bogdan Timofte authored a month ago
72
            let meter = Meter(model: model, with: btSerial)
73
            m[peripheral.identifier] = meter
Bogdan Timofte authored 2 months ago
74
            appData.meters = m
Bogdan Timofte authored a month ago
75
            appData.restoreChargeMonitoringStateIfNeeded(for: meter)
Bogdan Timofte authored 2 months ago
76
        } else if let meter = appData.meters[peripheral.identifier] {
77
            meter.lastSeen = Date()
Bogdan Timofte authored 2 months ago
78
            meter.btSerial.updateRSSI(RSSI.intValue)
Bogdan Timofte authored 2 months ago
79
            let macAddress = meter.btSerial.macAddress.description
80
            if meter.name == macAddress, let syncedName = appData.meterName(for: macAddress), syncedName != macAddress {
81
                meter.updateNameFromStore(syncedName)
82
            }
Bogdan Timofte authored a month ago
83
            appData.restoreChargeMonitoringStateIfNeeded(for: meter)
Bogdan Timofte authored 2 months ago
84
            if peripheral.delegate == nil {
85
                peripheral.delegate = meter.btSerial
86
            }
87
        }
88
    }
89

            
90
    private func resolvedPeripheralName(for peripheral: CBPeripheral, advertising advertismentData: [String : Any]) -> String? {
91
        let candidates = [
92
            (advertismentData[CBAdvertisementDataLocalNameKey] as? String),
93
            peripheral.name
94
        ]
95

            
96
        for candidate in candidates {
97
            if let trimmed = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty {
98
                return trimmed
Bogdan Timofte authored 2 months ago
99
            }
100
        }
Bogdan Timofte authored 2 months ago
101

            
102
        return nil
103
    }
104

            
105
    private func resolvedManufacturerData(from advertismentData: [String : Any]) -> Data? {
106
        if let data = advertismentData[CBAdvertisementDataManufacturerDataKey] as? Data {
107
            return data
Bogdan Timofte authored 2 months ago
108
        }
Bogdan Timofte authored 2 months ago
109
        if let data = advertismentData["kCBAdvDataManufacturerData"] as? Data {
110
            return data
111
        }
112
        return nil
Bogdan Timofte authored 2 months ago
113
    }
114
}
115

            
116
extension BluetoothManager : CBCentralManagerDelegate {
117
    // MARK:  CBCentralManager state Changed
118
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
119
        managerState = central.state;
120
        track("\(central.state)")
Bogdan Timofte authored 2 months ago
121
        for meter in appData.meters.values {
122
            meter.btSerial.centralStateChanged(to: central.state)
123
        }
Bogdan Timofte authored 2 months ago
124

            
125
        switch central.state {
126
        case .poweredOff:
Bogdan Timofte authored 2 months ago
127
            scanStartedAt = nil
Bogdan Timofte authored 2 months ago
128
            advertisementDataCache.clear()
Bogdan Timofte authored 2 months ago
129
            track("Bluetooth is Off. How should I behave?")
130
        case .poweredOn:
Bogdan Timofte authored 2 months ago
131
            scanStartedAt = Date()
Bogdan Timofte authored 2 months ago
132
            track("Bluetooth is On... Start scanning...")
133
            // note that "didDisconnectPeripheral" won't be called if BLE is turned off while connected
134
            // connectedPeripheral = nil
135
            // pendingPeripheral = nil
Bogdan Timofte authored 2 months ago
136
            scanForMeters()
Bogdan Timofte authored 2 months ago
137
        case .resetting:
Bogdan Timofte authored 2 months ago
138
            scanStartedAt = nil
Bogdan Timofte authored 2 months ago
139
            advertisementDataCache.clear()
Bogdan Timofte authored 2 months ago
140
            track("Bluetooth is reseting... . Whatever that means.")
141
        case .unauthorized:
Bogdan Timofte authored 2 months ago
142
            scanStartedAt = nil
Bogdan Timofte authored 2 months ago
143
            advertisementDataCache.clear()
Bogdan Timofte authored 2 months ago
144
            track("Bluetooth is not authorized.")
145
        case .unknown:
Bogdan Timofte authored 2 months ago
146
            scanStartedAt = nil
Bogdan Timofte authored 2 months ago
147
            advertisementDataCache.clear()
Bogdan Timofte authored 2 months ago
148
            track("Bluetooth is in an unknown state.")
149
        case .unsupported:
Bogdan Timofte authored 2 months ago
150
            scanStartedAt = nil
Bogdan Timofte authored 2 months ago
151
            advertisementDataCache.clear()
Bogdan Timofte authored 2 months ago
152
            track("Bluetooth not supported by device")
153
        default:
Bogdan Timofte authored 2 months ago
154
            scanStartedAt = nil
Bogdan Timofte authored 2 months ago
155
            advertisementDataCache.clear()
Bogdan Timofte authored 2 months ago
156
            track("Bluetooth is in a state never seen before!")
157
        }
158
    }
159

            
160
    // MARK:  CBCentralManager didDiscover peripheral
161
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
162
        let completeAdvertisementData = self.advertisementDataCache.append(peripheral: peripheral, advertisementData: advertisementData)
163
        //track("Device discoverded UUID: '\(peripheral.identifier)' named '\(peripheral.name ?? "Unknown")'); RSSI: \(RSSI) dBm; Advertisment data: \(advertisementData)")
164
        discoveredMeter(peripheral: peripheral, advertising: completeAdvertisementData, rssi: RSSI )
165
    }
166

            
167
    // MARK:  CBCentralManager didConnect peripheral
168
    internal func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
169
        //track("Connected to peripheral: '\(peripheral.identifier)'")
170
        if let usbMeter = appData.meters[peripheral.identifier] {
171
            usbMeter.btSerial.connectionEstablished()
172
        }
173
        else {
Bogdan Timofte authored 2 months ago
174
            track("Connected to meter with UUID: '\(peripheral.identifier)'")
Bogdan Timofte authored 2 months ago
175
        }
176
    }
177

            
178
    // MARK:  CBCentralManager didDisconnectPeripheral peripheral
179
    internal func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
180
        track("Disconnected from peripheral: '\(peripheral.identifier)' with error: \(error.debugDescription)")
181
        if let usbMeter = appData.meters[peripheral.identifier] {
182
            usbMeter.btSerial.connectionClosed()
183
        }
184
        else {
Bogdan Timofte authored 2 months ago
185
            track("Disconnected from meter with UUID: '\(peripheral.identifier)'")
Bogdan Timofte authored 2 months ago
186
        }
187
    }
Bogdan Timofte authored 2 months ago
188

            
189
    internal func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
190
        track("Failed to connect to peripheral: '\(peripheral.identifier)' with error: \(error.debugDescription)")
191
        if let usbMeter = appData.meters[peripheral.identifier] {
192
            usbMeter.btSerial.connectionClosed()
193
        } else {
Bogdan Timofte authored 2 months ago
194
            track("Failed to connect to meter with UUID: '\(peripheral.identifier)'")
Bogdan Timofte authored 2 months ago
195
        }
196
    }
Bogdan Timofte authored 2 months ago
197
}
198

            
199
private class AdvertisementDataCache {
200

            
Bogdan Timofte authored 2 months ago
201
    private var map = [UUID: [String: Any]]()
Bogdan Timofte authored 2 months ago
202

            
203
    func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
204
        var ad = (map[peripheral.identifier]) ?? [String: Any]()
Bogdan Timofte authored 2 months ago
205
        if let localName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines), !localName.isEmpty {
206
            ad[CBAdvertisementDataLocalNameKey] = localName
207
        }
Bogdan Timofte authored 2 months ago
208
        for (key, value) in advertisementData {
209
            ad[key] = value
210
        }
211
        map[peripheral.identifier] = ad
212
        return ad
213
    }
214

            
215
    func clear() {
216
        map.removeAll()
217
    }
218
}