USB-Meter / USB Meter / Model / BluetoothManager.swift
Newer Older
175 lines | 7.043kb
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
15

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

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

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

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

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

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

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

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

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

            
82
        return nil
83
    }
84

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

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

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

            
126
    // MARK:  CBCentralManager didDiscover peripheral
127
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
128
        let completeAdvertisementData = self.advertisementDataCache.append(peripheral: peripheral, advertisementData: advertisementData)
129
        //track("Device discoverded UUID: '\(peripheral.identifier)' named '\(peripheral.name ?? "Unknown")'); RSSI: \(RSSI) dBm; Advertisment data: \(advertisementData)")
130
        discoveredMeter(peripheral: peripheral, advertising: completeAdvertisementData, rssi: RSSI )
131
    }
132

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

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

            
156
private class AdvertisementDataCache {
157

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

            
160
    func append(peripheral: CBPeripheral, advertisementData: [String: Any]) -> [String: Any] {
161
        var ad = (map[peripheral.identifier]) ?? [String: Any]()
Bogdan Timofte authored 2 weeks ago
162
        if let localName = peripheral.name?.trimmingCharacters(in: .whitespacesAndNewlines), !localName.isEmpty {
163
            ad[CBAdvertisementDataLocalNameKey] = localName
164
        }
Bogdan Timofte authored 2 weeks ago
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
}