Showing 3 changed files with 152 additions and 40 deletions
+21 -5
USB Meter/Model/BluetoothManager.swift
@@ -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 {
+65 -15
USB Meter/Model/BluetoothSerial.swift
@@ -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
-
+66 -20
USB Meter/Model/Meter.swift
@@ -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
 }