@@ -10,6 +10,7 @@ import CoreBluetooth |
||
| 10 | 10 |
|
| 11 | 11 |
class BluetoothManager : NSObject, ObservableObject {
|
| 12 | 12 |
private var manager: CBCentralManager? |
| 13 |
+ private var isStarting = false |
|
| 13 | 14 |
private var advertisementDataCache = AdvertisementDataCache() |
| 14 | 15 |
@Published var managerState = CBManagerState.unknown |
| 15 | 16 |
@Published private(set) var scanStartedAt: Date? |
@@ -19,11 +20,19 @@ class BluetoothManager : NSObject, ObservableObject {
|
||
| 19 | 20 |
} |
| 20 | 21 |
|
| 21 | 22 |
func start() {
|
| 22 |
- guard manager == nil else {
|
|
| 23 |
+ guard manager == nil, !isStarting else {
|
|
| 23 | 24 |
return |
| 24 | 25 |
} |
| 26 |
+ isStarting = true |
|
| 25 | 27 |
track("Starting Bluetooth manager and requesting authorization if needed")
|
| 26 |
- manager = CBCentralManager(delegate: self, queue: nil) |
|
| 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 |
+ } |
|
| 27 | 36 |
} |
| 28 | 37 |
|
| 29 | 38 |
private func scanForMeters() {
|
@@ -110,9 +119,7 @@ extension BluetoothManager : CBCentralManagerDelegate {
|
||
| 110 | 119 |
// note that "didDisconnectPeripheral" won't be called if BLE is turned off while connected |
| 111 | 120 |
// connectedPeripheral = nil |
| 112 | 121 |
// pendingPeripheral = nil |
| 113 |
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
| 114 |
- self?.scanForMeters() |
|
| 115 |
- } |
|
| 122 |
+ scanForMeters() |
|
| 116 | 123 |
case .resetting: |
| 117 | 124 |
scanStartedAt = nil |
| 118 | 125 |
track("Bluetooth is reseting... . Whatever that means.")
|
@@ -159,6 +166,15 @@ extension BluetoothManager : CBCentralManagerDelegate {
|
||
| 159 | 166 |
track("Disconnected from unknown meter with UUID: '\(peripheral.identifier)'")
|
| 160 | 167 |
} |
| 161 | 168 |
} |
| 169 |
+ |
|
| 170 |
+ internal func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
|
| 171 |
+ track("Failed to connect to peripheral: '\(peripheral.identifier)' with error: \(error.debugDescription)")
|
|
| 172 |
+ if let usbMeter = appData.meters[peripheral.identifier] {
|
|
| 173 |
+ usbMeter.btSerial.connectionClosed() |
|
| 174 |
+ } else {
|
|
| 175 |
+ track("Failed to connect to unknown meter with UUID: '\(peripheral.identifier)'")
|
|
| 176 |
+ } |
|
| 177 |
+ } |
|
| 162 | 178 |
} |
| 163 | 179 |
|
| 164 | 180 |
private class AdvertisementDataCache {
|
@@ -66,12 +66,32 @@ final class BluetoothSerial : NSObject, ObservableObject {
|
||
| 66 | 66 |
super.init() |
| 67 | 67 |
peripheral.delegate = self |
| 68 | 68 |
} |
| 69 |
+ |
|
| 70 |
+ private func resetCommunicationState(reason: String, clearCharacteristics: Bool) {
|
|
| 71 |
+ if wdTimer != nil {
|
|
| 72 |
+ track("Reset communication state (\(reason)) - invalidating watchdog")
|
|
| 73 |
+ } |
|
| 74 |
+ wdTimer?.invalidate() |
|
| 75 |
+ wdTimer = nil |
|
| 76 |
+ |
|
| 77 |
+ if expectedResponseLength != 0 || !buffer.isEmpty {
|
|
| 78 |
+ track("Reset communication state (\(reason)) - expected: \(expectedResponseLength), buffered: \(buffer.count)")
|
|
| 79 |
+ } |
|
| 80 |
+ expectedResponseLength = 0 |
|
| 81 |
+ buffer.removeAll() |
|
| 82 |
+ |
|
| 83 |
+ if clearCharacteristics {
|
|
| 84 |
+ writeCharacteristic = nil |
|
| 85 |
+ notifyCharacteristic = nil |
|
| 86 |
+ } |
|
| 87 |
+ } |
|
| 69 | 88 |
|
| 70 | 89 |
func connect() {
|
| 71 | 90 |
administrativeState = .up |
| 72 | 91 |
if operationalState < .peripheralConnected {
|
| 92 |
+ resetCommunicationState(reason: "connect()", clearCharacteristics: true) |
|
| 73 | 93 |
operationalState = .peripheralConnectionPending |
| 74 |
- track("Connect caled")
|
|
| 94 |
+ track("Connect called for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
|
|
| 75 | 95 |
manager.connect(peripheral, options: nil) |
| 76 | 96 |
} else {
|
| 77 | 97 |
track("Peripheral allready connected: \(operationalState)")
|
@@ -79,9 +99,11 @@ final class BluetoothSerial : NSObject, ObservableObject {
|
||
| 79 | 99 |
} |
| 80 | 100 |
|
| 81 | 101 |
func disconnect() {
|
| 82 |
- if operationalState >= .peripheralConnected {
|
|
| 102 |
+ administrativeState = .down |
|
| 103 |
+ resetCommunicationState(reason: "disconnect()", clearCharacteristics: true) |
|
| 104 |
+ if peripheral.state != .disconnected || operationalState != .peripheralNotConnected {
|
|
| 105 |
+ track("Disconnect requested for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
|
|
| 83 | 106 |
manager.cancelPeripheralConnection(peripheral) |
| 84 |
- buffer.removeAll() |
|
| 85 | 107 |
} |
| 86 | 108 |
} |
| 87 | 109 |
|
@@ -121,17 +143,16 @@ final class BluetoothSerial : NSObject, ObservableObject {
|
||
| 121 | 143 |
} |
| 122 | 144 |
|
| 123 | 145 |
func connectionEstablished () {
|
| 124 |
- track("")
|
|
| 146 |
+ resetCommunicationState(reason: "connectionEstablished()", clearCharacteristics: true) |
|
| 147 |
+ track("Connection established for '\(peripheral.identifier)'")
|
|
| 125 | 148 |
operationalState = .peripheralConnected |
| 126 | 149 |
peripheral.discoverServices(BluetoothRadioServicesUUIDS[radio]) |
| 127 | 150 |
} |
| 128 | 151 |
|
| 129 | 152 |
func connectionClosed () {
|
| 130 |
- track("")
|
|
| 153 |
+ track("Connection closed for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
|
|
| 154 |
+ resetCommunicationState(reason: "connectionClosed()", clearCharacteristics: true) |
|
| 131 | 155 |
operationalState = .peripheralNotConnected |
| 132 |
- expectedResponseLength = 0 |
|
| 133 |
- writeCharacteristic = nil |
|
| 134 |
- notifyCharacteristic = nil |
|
| 135 | 156 |
} |
| 136 | 157 |
|
| 137 | 158 |
func setWDT() {
|
@@ -144,9 +165,14 @@ final class BluetoothSerial : NSObject, ObservableObject {
|
||
| 144 | 165 |
} |
| 145 | 166 |
|
| 146 | 167 |
private func refreshOperationalStateIfReady() {
|
| 147 |
- guard notifyCharacteristic != nil, writeCharacteristic != nil else {
|
|
| 168 |
+ guard let notifyCharacteristic, let writeCharacteristic else {
|
|
| 148 | 169 |
return |
| 149 | 170 |
} |
| 171 |
+ guard notifyCharacteristic.isNotifying else {
|
|
| 172 |
+ track("Waiting for notifications on '\(notifyCharacteristic.uuid)' before marking peripheral ready")
|
|
| 173 |
+ return |
|
| 174 |
+ } |
|
| 175 |
+ track("Peripheral ready with notify '\(notifyCharacteristic.uuid)' and write '\(writeCharacteristic.uuid)'")
|
|
| 150 | 176 |
operationalState = .peripheralReady |
| 151 | 177 |
} |
| 152 | 178 |
|
@@ -249,6 +275,16 @@ extension BluetoothSerial : CBPeripheralDelegate {
|
||
| 249 | 275 |
track("Radio \(radio) Not Implemented!")
|
| 250 | 276 |
} |
| 251 | 277 |
} |
| 278 |
+ |
|
| 279 |
+ func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
|
|
| 280 |
+ if error != nil {
|
|
| 281 |
+ track("Error updating notification state for '\(characteristic.uuid)': \(error!)")
|
|
| 282 |
+ } |
|
| 283 |
+ track("Notification state updated for '\(characteristic.uuid)' - isNotifying: \(characteristic.isNotifying)")
|
|
| 284 |
+ if characteristic.uuid == notifyCharacteristic?.uuid {
|
|
| 285 |
+ refreshOperationalStateIfReady() |
|
| 286 |
+ } |
|
| 287 |
+ } |
|
| 252 | 288 |
|
| 253 | 289 |
// MARK: didUpdateValueFor |
| 254 | 290 |
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
@@ -256,7 +292,23 @@ extension BluetoothSerial : CBPeripheralDelegate {
|
||
| 256 | 292 |
if error != nil {
|
| 257 | 293 |
track( "Error: \(error!)" ) |
| 258 | 294 |
} |
| 259 |
- buffer.append( characteristic.value ?? Data() ) |
|
| 295 |
+ let incomingData = characteristic.value ?? Data() |
|
| 296 |
+ guard !incomingData.isEmpty else {
|
|
| 297 |
+ track("Received empty update for '\(characteristic.uuid)'")
|
|
| 298 |
+ return |
|
| 299 |
+ } |
|
| 300 |
+ guard expectedResponseLength > 0 else {
|
|
| 301 |
+ if !buffer.isEmpty {
|
|
| 302 |
+ track("Dropping unsolicited data (\(incomingData.count) bytes) for '\(characteristic.uuid)' with residual buffer: \(buffer.count)")
|
|
| 303 |
+ buffer.removeAll() |
|
| 304 |
+ } else {
|
|
| 305 |
+ track("Dropping unsolicited data (\(incomingData.count) bytes) for '\(characteristic.uuid)' while no response is expected")
|
|
| 306 |
+ } |
|
| 307 |
+ return |
|
| 308 |
+ } |
|
| 309 |
+ |
|
| 310 |
+ let previousBufferCount = buffer.count |
|
| 311 |
+ buffer.append(incomingData) |
|
| 260 | 312 |
// track("\n\(buffer.hexEncodedStringValue)")
|
| 261 | 313 |
switch buffer.count {
|
| 262 | 314 |
case let x where x < expectedResponseLength: |
@@ -271,10 +323,9 @@ extension BluetoothSerial : CBPeripheralDelegate {
|
||
| 271 | 323 |
buffer.removeAll() |
| 272 | 324 |
case let x where x > expectedResponseLength: |
| 273 | 325 |
// MARK: De unde stim că asta a fost tot? Probabil o deconectare ar rezolva problema |
| 274 |
- wdTimer?.invalidate() |
|
| 275 |
- expectedResponseLength = 0 |
|
| 276 |
- buffer.removeAll() |
|
| 277 |
- track("Buffer Overflow")
|
|
| 326 |
+ let expectedLength = expectedResponseLength |
|
| 327 |
+ track("Buffer overflow for '\(characteristic.uuid)'. Chunk: \(incomingData.count), previous buffer: \(previousBufferCount), total: \(x), expected: \(expectedLength). Disconnecting to recover.")
|
|
| 328 |
+ disconnect() |
|
| 278 | 329 |
default: |
| 279 | 330 |
track("This is not possible!")
|
| 280 | 331 |
} |
@@ -301,4 +352,3 @@ protocol SerialPortDelegate: AnyObject {
|
||
| 301 | 352 |
// MARK: SerialPortDelegate Optionals |
| 302 | 353 |
extension SerialPortDelegate {
|
| 303 | 354 |
} |
| 304 |
- |
|
@@ -88,22 +88,30 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 88 | 88 |
|
| 89 | 89 |
@Published var operationalState = OperationalState.peripheralNotConnected {
|
| 90 | 90 |
didSet {
|
| 91 |
+ guard operationalState != oldValue else { return }
|
|
| 92 |
+ track("\(name) - Operational state changed from \(oldValue) to \(operationalState)")
|
|
| 91 | 93 |
switch operationalState {
|
| 92 | 94 |
case .notPresent: |
| 95 |
+ cancelPendingDataDumpRequest(reason: "meter missing") |
|
| 93 | 96 |
break |
| 94 | 97 |
case .peripheralNotConnected: |
| 98 |
+ cancelPendingDataDumpRequest(reason: "peripheral disconnected") |
|
| 99 |
+ if !commandQueue.isEmpty {
|
|
| 100 |
+ track("\(name) - Clearing \(commandQueue.count) queued commands after disconnect")
|
|
| 101 |
+ commandQueue.removeAll() |
|
| 102 |
+ } |
|
| 95 | 103 |
if enableAutoConnect {
|
| 96 | 104 |
track("\(name) - Reconnecting...")
|
| 97 | 105 |
btSerial.connect() |
| 98 | 106 |
} |
| 99 | 107 |
case .peripheralConnectionPending: |
| 108 |
+ cancelPendingDataDumpRequest(reason: "connection pending") |
|
| 100 | 109 |
break |
| 101 | 110 |
case .peripheralConnected: |
| 111 |
+ cancelPendingDataDumpRequest(reason: "services not ready yet") |
|
| 102 | 112 |
break |
| 103 | 113 |
case .peripheralReady: |
| 104 |
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Change `2.0` to the desired number of seconds.
|
|
| 105 |
- self.dataDumpRequest() |
|
| 106 |
- } |
|
| 114 |
+ scheduleDataDumpRequest(after: 0.5, reason: "peripheral ready") |
|
| 107 | 115 |
case .comunicating: |
| 108 | 116 |
break |
| 109 | 117 |
case .dataIsAvailable: |
@@ -395,6 +403,7 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 395 | 403 |
|
| 396 | 404 |
private var commandQueue: [Data] = [] |
| 397 | 405 |
private var dataDumpRequestTimestamp = Date() |
| 406 |
+ private var pendingDataDumpWorkItem: DispatchWorkItem? |
|
| 398 | 407 |
|
| 399 | 408 |
class DataGroupRecord {
|
| 400 | 409 |
@Published var ah: Double |
@@ -511,8 +520,32 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 511 | 520 |
tc66TemperatureUnitPreference = persistedPreference |
| 512 | 521 |
} |
| 513 | 522 |
} |
| 523 |
+ |
|
| 524 |
+ private func cancelPendingDataDumpRequest(reason: String) {
|
|
| 525 |
+ guard let pendingDataDumpWorkItem else { return }
|
|
| 526 |
+ track("\(name) - Cancel scheduled data request (\(reason))")
|
|
| 527 |
+ pendingDataDumpWorkItem.cancel() |
|
| 528 |
+ self.pendingDataDumpWorkItem = nil |
|
| 529 |
+ } |
|
| 530 |
+ |
|
| 531 |
+ private func scheduleDataDumpRequest(after delay: TimeInterval, reason: String) {
|
|
| 532 |
+ cancelPendingDataDumpRequest(reason: "reschedule") |
|
| 533 |
+ |
|
| 534 |
+ let workItem = DispatchWorkItem { [weak self] in
|
|
| 535 |
+ guard let self else { return }
|
|
| 536 |
+ self.pendingDataDumpWorkItem = nil |
|
| 537 |
+ self.dataDumpRequest() |
|
| 538 |
+ } |
|
| 539 |
+ pendingDataDumpWorkItem = workItem |
|
| 540 |
+ track("\(name) - Schedule data request in \(String(format: "%.2f", delay))s (\(reason))")
|
|
| 541 |
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) |
|
| 542 |
+ } |
|
| 514 | 543 |
|
| 515 | 544 |
func dataDumpRequest() {
|
| 545 |
+ guard operationalState >= .peripheralReady else {
|
|
| 546 |
+ track("\(name) - Skip data request while state is \(operationalState)")
|
|
| 547 |
+ return |
|
| 548 |
+ } |
|
| 516 | 549 |
if commandQueue.isEmpty {
|
| 517 | 550 |
switch model {
|
| 518 | 551 |
case .UM25C: |
@@ -528,9 +561,7 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 528 | 561 |
track("Request delayed for: \(commandQueue.first!.hexEncodedStringValue)")
|
| 529 | 562 |
btSerial.write( commandQueue.first! ) |
| 530 | 563 |
commandQueue.removeFirst() |
| 531 |
- DispatchQueue.main.asyncAfter( deadline: .now() + 1 ) {
|
|
| 532 |
- self.dataDumpRequest() |
|
| 533 |
- } |
|
| 564 |
+ scheduleDataDumpRequest(after: 1, reason: "queued command") |
|
| 534 | 565 |
} |
| 535 | 566 |
} |
| 536 | 567 |
|
@@ -797,23 +828,38 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 797 | 828 |
extension Meter : SerialPortDelegate {
|
| 798 | 829 |
|
| 799 | 830 |
func opertionalStateChanged(to serialPortOperationalState: BluetoothSerial.OperationalState) {
|
| 800 |
- lastSeen = Date() |
|
| 801 |
- //track("\(name) - \(serialPortOperationalState)")
|
|
| 802 |
- switch serialPortOperationalState {
|
|
| 803 |
- case .peripheralNotConnected: |
|
| 804 |
- operationalState = .peripheralNotConnected |
|
| 805 |
- case .peripheralConnectionPending: |
|
| 806 |
- operationalState = .peripheralConnectionPending |
|
| 807 |
- case .peripheralConnected: |
|
| 808 |
- operationalState = .peripheralConnected |
|
| 809 |
- case .peripheralReady: |
|
| 810 |
- operationalState = .peripheralReady |
|
| 831 |
+ let applyStateChange = {
|
|
| 832 |
+ self.lastSeen = Date() |
|
| 833 |
+ switch serialPortOperationalState {
|
|
| 834 |
+ case .peripheralNotConnected: |
|
| 835 |
+ self.operationalState = .peripheralNotConnected |
|
| 836 |
+ case .peripheralConnectionPending: |
|
| 837 |
+ self.operationalState = .peripheralConnectionPending |
|
| 838 |
+ case .peripheralConnected: |
|
| 839 |
+ self.operationalState = .peripheralConnected |
|
| 840 |
+ case .peripheralReady: |
|
| 841 |
+ self.operationalState = .peripheralReady |
|
| 842 |
+ } |
|
| 843 |
+ } |
|
| 844 |
+ |
|
| 845 |
+ if Thread.isMainThread {
|
|
| 846 |
+ applyStateChange() |
|
| 847 |
+ } else {
|
|
| 848 |
+ DispatchQueue.main.async(execute: applyStateChange) |
|
| 811 | 849 |
} |
| 812 | 850 |
} |
| 813 | 851 |
|
| 814 | 852 |
func didReceiveData(_ data: Data) {
|
| 815 |
- lastSeen = Date() |
|
| 816 |
- operationalState = .comunicating |
|
| 817 |
- parseData(from: data) |
|
| 853 |
+ let applyData = {
|
|
| 854 |
+ self.lastSeen = Date() |
|
| 855 |
+ self.operationalState = .comunicating |
|
| 856 |
+ self.parseData(from: data) |
|
| 857 |
+ } |
|
| 858 |
+ |
|
| 859 |
+ if Thread.isMainThread {
|
|
| 860 |
+ applyData() |
|
| 861 |
+ } else {
|
|
| 862 |
+ DispatchQueue.main.async(execute: applyData) |
|
| 863 |
+ } |
|
| 818 | 864 |
} |
| 819 | 865 |
} |