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

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

            
Bogdan Timofte authored 2 weeks ago
21
    func start() {
22
        guard manager == nil else {
23
            return
24
        }
25
        track("Starting Bluetooth manager and requesting authorization if needed")
26
        manager = CBCentralManager(delegate: self, queue: nil)
27
    }
Bogdan Timofte authored 2 weeks ago
28

            
29
    private func scanForMeters() {
Bogdan Timofte authored 2 weeks ago
30
        guard let manager else {
31
            track("Scan requested before Bluetooth manager was started")
32
            return
33
        }
Bogdan Timofte authored 2 weeks ago
34
        guard manager.state == .poweredOn else {
35
            track( "Scan requested but Bluetooth state is \(manager.state)")
36
            return
37
        }
38
        //manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ])
39
        manager.scanForPeripherals(withServices: allBluetoothRadioServices(), options: [ CBCentralManagerScanOptionAllowDuplicatesKey: true ])
40
    }
41

            
42
    func discoveredMeter(peripheral: CBPeripheral, advertising advertismentData: [String : Any], rssi RSSI: NSNumber) {
Bogdan Timofte authored 2 weeks ago
43
        guard let peripheralName = resolvedPeripheralName(for: peripheral, advertising: advertismentData) else {
44
            return
45
        }
46
        guard let manufacturerData = resolvedManufacturerData(from: advertismentData), manufacturerData.count > 2 else {
47
            return
48
        }
49

            
Bogdan Timofte authored 2 weeks ago
50
        guard let model = Model.byPeripheralName[peripheralName] else {
Bogdan Timofte authored 2 weeks ago
51
            return
52
        }
53

            
54
        let macAddress = MACAddress(from: manufacturerData.suffix(from: 2))
55

            
56
        if appData.meters[peripheral.identifier] == nil {
57
            track("adding new USB Meter named '\(peripheralName)' with MAC Address: '\(macAddress)'")
Bogdan Timofte authored 2 weeks ago
58
            let btSerial = BluetoothSerial(peripheral: peripheral, radio: model.radio, with: macAddress, managedBy: manager!, RSSI: RSSI.intValue)
Bogdan Timofte authored 2 weeks ago
59
            var m = appData.meters
60
            m[peripheral.identifier] = Meter(model: model, with: btSerial)
61
            appData.meters = m
62
        } else if let meter = appData.meters[peripheral.identifier] {
63
            meter.lastSeen = Date()
64
            meter.btSerial.RSSI = RSSI.intValue
65
            if peripheral.delegate == nil {
66
                peripheral.delegate = meter.btSerial
67
            }
68
        }
69
    }
70

            
71
    private func resolvedPeripheralName(for peripheral: CBPeripheral, advertising advertismentData: [String : Any]) -> String? {
72
        let candidates = [
73
            (advertismentData[CBAdvertisementDataLocalNameKey] as? String),
74
            peripheral.name
75
        ]
76

            
77
        for candidate in candidates {
78
            if let trimmed = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty {
79
                return trimmed
Bogdan Timofte authored 2 weeks ago
80
            }
81
        }
Bogdan Timofte authored 2 weeks ago
82

            
83
        return nil
84
    }
85

            
86
    private func resolvedManufacturerData(from advertismentData: [String : Any]) -> Data? {
87
        if let data = advertismentData[CBAdvertisementDataManufacturerDataKey] as? Data {
88
            return data
Bogdan Timofte authored 2 weeks ago
89
        }
Bogdan Timofte authored 2 weeks ago
90
        if let data = advertismentData["kCBAdvDataManufacturerData"] as? Data {
91
            return data
92
        }
93
        return nil
Bogdan Timofte authored 2 weeks ago
94
    }
95
}
96

            
97
extension BluetoothManager : CBCentralManagerDelegate {
98
    // MARK:  CBCentralManager state Changed
99
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
100
        managerState = central.state;
101
        track("\(central.state)")
102

            
103
        switch central.state {
104
        case .poweredOff:
Bogdan Timofte authored 2 weeks ago
105
            scanStartedAt = nil
Bogdan Timofte authored 2 weeks ago
106
            track("Bluetooth is Off. How should I behave?")
107
        case .poweredOn:
Bogdan Timofte authored 2 weeks ago
108
            scanStartedAt = Date()
Bogdan Timofte authored 2 weeks ago
109
            track("Bluetooth is On... Start scanning...")
110
            // note that "didDisconnectPeripheral" won't be called if BLE is turned off while connected
111
            // connectedPeripheral = nil
112
            // pendingPeripheral = nil
113
            DispatchQueue.global(qos: .userInitiated).async { [weak self] in
114
                self?.scanForMeters()
115
            }
116
        case .resetting:
Bogdan Timofte authored 2 weeks ago
117
            scanStartedAt = nil
Bogdan Timofte authored 2 weeks ago
118
            track("Bluetooth is reseting... . Whatever that means.")
119
        case .unauthorized:
Bogdan Timofte authored 2 weeks ago
120
            scanStartedAt = nil
Bogdan Timofte authored 2 weeks ago
121
            track("Bluetooth is not authorized.")
122
        case .unknown:
Bogdan Timofte authored 2 weeks ago
123
            scanStartedAt = nil
Bogdan Timofte authored 2 weeks ago
124
            track("Bluetooth is in an unknown state.")
125
        case .unsupported:
Bogdan Timofte authored 2 weeks ago
126
            scanStartedAt = nil
Bogdan Timofte authored 2 weeks ago
127
            track("Bluetooth not supported by device")
128
        default:
Bogdan Timofte authored 2 weeks ago
129
            scanStartedAt = nil
Bogdan Timofte authored 2 weeks ago
130
            track("Bluetooth is in a state never seen before!")
131
        }
132
    }
133

            
134
    // MARK:  CBCentralManager didDiscover peripheral
135
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
136
        let completeAdvertisementData = self.advertisementDataCache.append(peripheral: peripheral, advertisementData: advertisementData)
137
        //track("Device discoverded UUID: '\(peripheral.identifier)' named '\(peripheral.name ?? "Unknown")'); RSSI: \(RSSI) dBm; Advertisment data: \(advertisementData)")
138
        discoveredMeter(peripheral: peripheral, advertising: completeAdvertisementData, rssi: RSSI )
139
    }
140

            
141
    // MARK:  CBCentralManager didConnect peripheral
142
    internal func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
143
        //track("Connected to peripheral: '\(peripheral.identifier)'")
144
        if let usbMeter = appData.meters[peripheral.identifier] {
145
            usbMeter.btSerial.connectionEstablished()
146
        }
147
        else {
148
            track("Connected to unknown meter with UUID: '\(peripheral.identifier)'")
149
        }
150
    }
151

            
152
    // MARK:  CBCentralManager didDisconnectPeripheral peripheral
153
    internal func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
154
        track("Disconnected from peripheral: '\(peripheral.identifier)' with error: \(error.debugDescription)")
155
        if let usbMeter = appData.meters[peripheral.identifier] {
156
            usbMeter.btSerial.connectionClosed()
157
        }
158
        else {
159
            track("Disconnected from unknown meter with UUID: '\(peripheral.identifier)'")
160
        }
161
    }
162
}
163

            
164
private class AdvertisementDataCache {
165

            
Bogdan Timofte authored 2 weeks ago
166
    private var map = [UUID: [String: Any]]()
Bogdan Timofte authored 2 weeks ago
167

            
168
    func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
169
        var ad = (map[peripheral.identifier]) ?? [String: Any]()
Bogdan Timofte authored 2 weeks ago
170
        if let localName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines), !localName.isEmpty {
171
            ad[CBAdvertisementDataLocalNameKey] = localName
172
        }
Bogdan Timofte authored 2 weeks ago
173
        for (key, value) in advertisementData {
174
            ad[key] = value
175
        }
176
        map[peripheral.identifier] = ad
177
        return ad
178
    }
179

            
180
    func clear() {
181
        map.removeAll()
182
    }
183
}