@@ -51,6 +51,13 @@ var pairedDeviceIdentifiers: Set<String> {
51
51
}
52
52
}
53
53
54
+ class BLEConnectionContext {
55
+ let identifier = UUID ( )
56
+ let semaphore = DispatchSemaphore ( value: 0 )
57
+ var readBuffer = Data ( )
58
+ var readBufferLock = NSLock ( )
59
+ }
60
+
54
61
class BluetoothManager : NSObject , ObservableObject , CBCentralManagerDelegate , CBPeripheralDelegate {
55
62
private var state : State = State (
56
63
bluetoothAvailable: false ,
@@ -70,9 +77,11 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
70
77
// This is for failed connections to not enter an infinite connect loop.
71
78
private var dontAutoConnectSet : Set < UUID > = [ ]
72
79
73
- private var readBuffer = Data ( )
74
- private let readBufferLock = NSLock ( ) // Ensure thread-safe buffer access
75
- private let semaphore = DispatchSemaphore ( value: 0 )
80
+ private var currentContext : BLEConnectionContext ?
81
+ // Locks access to the `currentContext` var only, not to its contents. This is important, as
82
+ // one can't keep the context locked while waiting for the semaphore, which would lead to a
83
+ // deadlock.
84
+ private let currentContextLock = NSLock ( )
76
85
77
86
override init ( ) {
78
87
super. init ( )
@@ -93,6 +102,9 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
93
102
state. discoveredPeripherals [ peripheralID] = metadata
94
103
state. scanning = false
95
104
updateBackendState ( )
105
+ currentContextLock. lock ( )
106
+ currentContext = BLEConnectionContext ( )
107
+ currentContextLock. unlock ( )
96
108
centralManager. connect ( metadata. peripheral, options: nil )
97
109
}
98
110
@@ -246,17 +258,24 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
246
258
return
247
259
}
248
260
261
+ currentContextLock. lock ( )
262
+ guard let ctx = currentContext else {
263
+ currentContextLock. unlock ( )
264
+ return
265
+ }
266
+ currentContextLock. unlock ( )
267
+
249
268
if characteristic == pReader, let data = characteristic. value {
250
269
if data. count != 64 {
251
270
print ( " BLE: ERROR, expected 64 bytes " )
252
271
}
253
272
print ( " BLE: received data: \( data. hexEncodedString ( ) ) " )
254
- readBufferLock. lock ( )
255
- readBuffer. append ( data)
256
- readBufferLock. unlock ( )
273
+ ctx . readBufferLock. lock ( )
274
+ ctx . readBuffer. append ( data)
275
+ ctx . readBufferLock. unlock ( )
257
276
258
277
// Signal the semaphore to unblock `readBlocking`
259
- semaphore. signal ( )
278
+ ctx . semaphore. signal ( )
260
279
}
261
280
if characteristic == pProduct {
262
281
print ( " BLE: product changed: \( String ( describing: parseProduct ( ) ) ) " )
@@ -281,13 +300,19 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
281
300
pWriter = nil
282
301
pProduct = nil
283
302
state. discoveredPeripherals. removeAll ( )
284
- isPaired = false
303
+ isPaired = false
285
304
updateBackendState ( )
286
305
287
306
// Have the backend scan right away, which will make it detect that we disconnected.
288
307
// Otherwise there would be up to a second of delay (the backend device manager scan interval).
289
308
MobileserverUsbUpdate ( )
290
309
310
+ // Unblock a pending readBlocking() call if there is one.
311
+ currentContextLock. lock ( )
312
+ currentContext? . semaphore. signal ( )
313
+ currentContext = nil
314
+ currentContextLock. unlock ( )
315
+
291
316
restartScan ( )
292
317
}
293
318
@@ -297,22 +322,46 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
297
322
handleDisconnect ( )
298
323
}
299
324
300
- func readBlocking( length: Int ) -> Data ? {
325
+ struct ReadError : Error {
326
+ let message : String
327
+ }
328
+
329
+ func readBlocking( length: Int ) throws -> Data {
301
330
if !isConnected( ) {
302
- return nil
331
+ throw ReadError ( message : " not connected " )
303
332
}
304
333
print ( " BLE: wants to read \( length) " )
305
334
335
+ currentContextLock. lock ( )
336
+ guard let ctx = currentContext else {
337
+ currentContextLock. unlock ( )
338
+ throw ReadError ( message: " no connection context " )
339
+ }
340
+ currentContextLock. unlock ( )
341
+
342
+ let currentID = ctx. identifier;
343
+
306
344
var data = Data ( )
307
345
308
346
// Loop until we've read the required amount of data
309
347
while data. count < length {
310
- // Block until BLE reader callback notifies us
311
- semaphore. wait ( )
312
- readBufferLock. lock ( )
313
- data. append ( readBuffer. prefix ( 64 ) )
314
- readBuffer = readBuffer. advanced ( by: 64 )
315
- readBufferLock. unlock ( )
348
+ // Block until BLE reader callback notifies us or the peripheral is disconnected.
349
+ ctx. semaphore. wait ( )
350
+
351
+ if !isConnected( ) {
352
+ throw ReadError ( message: " the peripheral has disconnected while reading " )
353
+ }
354
+ currentContextLock. lock ( )
355
+ let exit = currentContext? . identifier != currentID
356
+ currentContextLock. unlock ( )
357
+ if exit {
358
+ throw ReadError ( message: " the peripheral has disconnected while reading " )
359
+ }
360
+
361
+ ctx. readBufferLock. lock ( )
362
+ data. append ( ctx. readBuffer. prefix ( 64 ) )
363
+ ctx. readBuffer = ctx. readBuffer. advanced ( by: 64 )
364
+ ctx. readBufferLock. unlock ( )
316
365
}
317
366
print ( " BLE: got \( data. count) " )
318
367
@@ -457,7 +506,7 @@ class BluetoothReadWriteCloser: NSObject, MobileserverGoReadWriteCloserInterface
457
506
}
458
507
459
508
func read( _ n: Int ) throws -> Data {
460
- return bluetoothManager. readBlocking ( length: n) !
509
+ try bluetoothManager. readBlocking ( length: n)
461
510
}
462
511
463
512
func write( _ data: Data ? , n: UnsafeMutablePointer < Int > ? ) throws {
0 commit comments