@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.3.0-beta.942

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +72 -10
  2. package/android/build.gradle +15 -0
  3. package/android/lc3Lib/build.gradle +6 -1
  4. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
  5. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
  7. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
  8. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
  9. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
  11. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
  15. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
  16. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
  17. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
  18. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
  19. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
  22. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
  24. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +4 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
  29. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
  30. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
  31. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
  32. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
  33. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
  34. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
  35. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +247 -0
  36. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
  37. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
  39. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
  40. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
  41. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
  42. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
  43. package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
  44. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
  45. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
  46. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
  47. package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
  48. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
  49. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
  50. package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
  51. package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
  52. package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
  53. package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
  54. package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
  55. package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
  56. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
  57. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
  58. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
  59. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
  60. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
  61. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
  62. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
  63. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
  64. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
  65. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
  66. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
  67. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
  68. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
  69. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +233 -0
  70. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
  71. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
  72. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
  73. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
  74. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
  75. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
  76. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
  77. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
  78. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
  79. package/build/BluetoothSdk.types.d.ts +16 -0
  80. package/build/BluetoothSdk.types.d.ts.map +1 -1
  81. package/build/BluetoothSdk.types.js +1 -0
  82. package/build/BluetoothSdk.types.js.map +1 -1
  83. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  84. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  85. package/build/_private/BluetoothSdkModule.js +5 -1
  86. package/build/_private/BluetoothSdkModule.js.map +1 -1
  87. package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
  88. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  89. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  90. package/build/_private/nativeLogConsole.d.ts +19 -0
  91. package/build/_private/nativeLogConsole.d.ts.map +1 -1
  92. package/build/_private/nativeLogConsole.js +12 -0
  93. package/build/_private/nativeLogConsole.js.map +1 -1
  94. package/build/generated/changelogCatalog.d.ts +5 -2
  95. package/build/generated/changelogCatalog.d.ts.map +1 -1
  96. package/build/generated/changelogCatalog.js +6 -2
  97. package/build/generated/changelogCatalog.js.map +1 -1
  98. package/build/generated/releaseMetadata.js +6 -6
  99. package/build/generated/releaseMetadata.js.map +1 -1
  100. package/build/index.d.ts.map +1 -1
  101. package/build/index.js +1 -0
  102. package/build/index.js.map +1 -1
  103. package/build/ota-transport/index.d.ts +4 -0
  104. package/build/ota-transport/index.d.ts.map +1 -1
  105. package/build/ota-transport/index.js +2 -0
  106. package/build/ota-transport/index.js.map +1 -1
  107. package/ios/BluetoothSdkModule.swift +17 -4
  108. package/ios/MentraBluetoothSDK.podspec +3 -2
  109. package/ios/Source/BluetoothBackgroundMode.swift +32 -0
  110. package/ios/Source/BluetoothSdkDefaults.swift +7 -2
  111. package/ios/Source/Bridge.swift +15 -2
  112. package/ios/Source/DeviceManager.swift +102 -66
  113. package/ios/Source/DeviceStore.swift +26 -7
  114. package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
  115. package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
  116. package/ios/Source/MentraBluetoothSDK.swift +313 -54
  117. package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
  118. package/ios/Source/internal/BluetoothAvailability.swift +6 -0
  119. package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
  120. package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
  121. package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
  122. package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
  123. package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
  124. package/ios/Source/requests/DisplayRequests.swift +1 -1
  125. package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
  126. package/ios/Source/sgcs/Ar99.swift +1 -2
  127. package/ios/Source/sgcs/G1.swift +68 -38
  128. package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
  129. package/ios/Source/sgcs/G2.swift +261 -247
  130. package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
  131. package/ios/Source/sgcs/Mach1.swift +19 -7
  132. package/ios/Source/sgcs/MentraLive.swift +272 -197
  133. package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
  134. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
  135. package/ios/Source/sgcs/MentraNex.swift +8 -31
  136. package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
  137. package/ios/Source/sgcs/Nimo.swift +280 -408
  138. package/ios/Source/sgcs/NimoCanvasCodec.swift +272 -0
  139. package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
  140. package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
  141. package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
  142. package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
  143. package/ios/Source/sgcs/SGCManager.swift +25 -1
  144. package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
  145. package/ios/Source/status/DeviceStatus.swift +1 -0
  146. package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
  147. package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
  148. package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
  149. package/ios/Source/utils/G1Text.swift +2 -24
  150. package/ios/Source/utils/G1TextSanitizer.swift +27 -0
  151. package/ios/Source/utils/G2Text.swift +11 -0
  152. package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
  153. package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
  154. package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
  155. package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
  156. package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
  157. package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
  158. package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
  159. package/ios/Tests/DashboardRenderingTests.swift +31 -0
  160. package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
  161. package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
  162. package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
  163. package/ios/Tests/G1PairingTests.swift +162 -0
  164. package/ios/Tests/G1TextSanitizerTests.swift +32 -0
  165. package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
  166. package/ios/Tests/G2TextTests.swift +10 -0
  167. package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
  168. package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
  169. package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
  170. package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
  171. package/ios/Tests/NimoCanvasCodecTests.swift +217 -0
  172. package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
  173. package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
  174. package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
  175. package/ios/Tests/NimoDiscoveryTests.swift +79 -0
  176. package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
  177. package/ios/Tests/VersionInfoCommandTests.swift +194 -0
  178. package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
  179. package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
  180. package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
  181. package/package.json +2 -2
  182. package/plugin/build/withIos.d.ts +2 -0
  183. package/plugin/build/withIos.js +12 -1
  184. package/src/BluetoothSdk.types.ts +15 -0
  185. package/src/_private/BluetoothSdkModule.ts +10 -1
  186. package/src/_private/MentraLocalNetworkModule.ts +28 -0
  187. package/src/_private/nativeLogConsole.ts +19 -0
  188. package/src/generated/changelogCatalog.ts +6 -2
  189. package/src/generated/releaseMetadata.ts +6 -6
  190. package/src/index.ts +1 -0
  191. package/src/ota-transport/index.ts +10 -1
@@ -7,8 +7,8 @@
7
7
 
8
8
  import Combine
9
9
  import CoreBluetooth
10
- import Foundation
11
10
  import CoreGraphics
11
+ import Foundation
12
12
 
13
13
  extension Data {
14
14
  func chunked(into size: Int) -> [Data] {
@@ -232,7 +232,8 @@ actor ReconnectionManager {
232
232
  }
233
233
 
234
234
  func start(onAttempt: @escaping @Sendable () async -> Bool) {
235
- stop()
235
+ // Multiple command failures must not restart the delay between attempts.
236
+ guard !isRunning else { return }
236
237
  attempts = 0
237
238
 
238
239
  task = Task {
@@ -258,6 +259,9 @@ actor ReconnectionManager {
258
259
  break
259
260
  }
260
261
  }
262
+ if !Task.isCancelled {
263
+ task = nil
264
+ }
261
265
  }
262
266
  }
263
267
 
@@ -312,7 +316,7 @@ class G1: NSObject, SGCManager {
312
316
 
313
317
  func queryGalleryStatus() {}
314
318
 
315
- func sendOtaStart(otaVersionUrl: String?) {}
319
+ func sendOtaStart(otaVersionUrl _: String?) {}
316
320
  func sendOtaQueryStatus() {}
317
321
 
318
322
  func ping() {}
@@ -450,13 +454,12 @@ class G1: NSObject, SGCManager {
450
454
  }
451
455
 
452
456
  private var centralManager: CBCentralManager?
457
+ private var scanRequested = false
453
458
 
454
459
  private var leftPeripheral: CBPeripheral?
455
460
  private var rightPeripheral: CBPeripheral?
456
461
  private var connectedDevices: [String: (CBPeripheral?, CBPeripheral?)] = [:]
457
462
  var lastConnectionTimestamp: Date = .distantPast
458
- private var leftInitialized: Bool = false
459
- private var rightInitialized: Bool = false
460
463
 
461
464
  private var leftGlassUUID: UUID? {
462
465
  get {
@@ -468,8 +471,10 @@ class G1: NSObject, SGCManager {
468
471
  set {
469
472
  if let newValue = newValue {
470
473
  UserDefaults.standard.set(newValue.uuidString, forKey: "leftGlassUUID")
474
+ UserDefaults.standard.set(DEVICE_SEARCH_ID, forKey: "leftGlassSearchID")
471
475
  } else {
472
476
  UserDefaults.standard.removeObject(forKey: "leftGlassUUID")
477
+ UserDefaults.standard.removeObject(forKey: "leftGlassSearchID")
473
478
  }
474
479
  }
475
480
  }
@@ -484,8 +489,10 @@ class G1: NSObject, SGCManager {
484
489
  set {
485
490
  if let newValue = newValue {
486
491
  UserDefaults.standard.set(newValue.uuidString, forKey: "rightGlassUUID")
492
+ UserDefaults.standard.set(DEVICE_SEARCH_ID, forKey: "rightGlassSearchID")
487
493
  } else {
488
494
  UserDefaults.standard.removeObject(forKey: "rightGlassUUID")
495
+ UserDefaults.standard.removeObject(forKey: "rightGlassSearchID")
489
496
  }
490
497
  }
491
498
  }
@@ -497,6 +504,8 @@ class G1: NSObject, SGCManager {
497
504
  }
498
505
 
499
506
  func forget() {
507
+ isDisconnecting = true
508
+ stopScan()
500
509
  Task {
501
510
  await heartbeatManager.stop()
502
511
  await reconnectionManager.stop()
@@ -700,11 +709,13 @@ class G1: NSObject, SGCManager {
700
709
 
701
710
  func connectById(_ id: String) {
702
711
  DEVICE_SEARCH_ID = "_" + id + "_"
712
+ scanRequested = true
703
713
  startScan()
704
714
  }
705
715
 
706
716
  func findCompatibleDevices() {
707
717
  DEVICE_SEARCH_ID = "NOT_SET"
718
+ scanRequested = true
708
719
  startScan()
709
720
  }
710
721
 
@@ -732,8 +743,7 @@ class G1: NSObject, SGCManager {
732
743
  )
733
744
  stopScan()
734
745
 
735
- // get battery status:
736
- getBatteryStatus()
746
+ // Battery requests start after both arms acknowledge initialization.
737
747
  return true
738
748
  }
739
749
 
@@ -981,11 +991,15 @@ class G1: NSObject, SGCManager {
981
991
  connected = leftReady && rightReady
982
992
  if fullyBooted {
983
993
  stopReconnectionTimer()
994
+ if !prevLeftReady || !prevRightReady {
995
+ getBatteryStatus()
996
+ }
984
997
  }
985
998
  }
986
999
 
987
1000
  func stopScan() {
988
- centralManager!.stopScan()
1001
+ scanRequested = false
1002
+ centralManager?.stopScan()
989
1003
  Bridge.log("G1: Stopped scanning for devices")
990
1004
  }
991
1005
 
@@ -1007,6 +1021,7 @@ class G1: NSObject, SGCManager {
1007
1021
 
1008
1022
  func disconnect() {
1009
1023
  isDisconnecting = true
1024
+ stopScan()
1010
1025
  leftGlassUUID = nil
1011
1026
  rightGlassUUID = nil
1012
1027
  stopReconnectionTimer()
@@ -1038,10 +1053,13 @@ class G1: NSObject, SGCManager {
1038
1053
  let data = Data(chunks[0])
1039
1054
  // CoreCommsService.log("SEND (\(side)) \(data.hexEncodedString())")
1040
1055
 
1041
- if isDisconnecting {
1042
- // forget whatever we were doing since we're disconnecting:
1043
- break
1044
- }
1056
+ // A queued command can outlive the link that enqueued it. Missing
1057
+ // transport during discovery/pairing is not an ACK timeout and must
1058
+ // not start a reconnect loop before CoreBluetooth finishes connecting.
1059
+ let peripheral = side == "L" ? leftPeripheral : rightPeripheral
1060
+ guard !isDisconnecting, peripheral?.state == .connected,
1061
+ getWriteCharacteristic(for: peripheral) != nil
1062
+ else { return }
1045
1063
 
1046
1064
  for i in 0 ..< chunks.count - 1 {
1047
1065
  let chunk = chunks[i]
@@ -1502,18 +1520,9 @@ extension G1 {
1502
1520
  /// don't call semaphore signals here as it's handled elswhere:
1503
1521
  private func handleInitResponse(from peripheral: CBPeripheral, success: Bool) {
1504
1522
  if peripheral == leftPeripheral {
1505
- leftInitialized = success
1506
- // CoreCommsService.log("G1: Left arm initialized: \(success)")
1507
- setReadiness(left: true, right: nil)
1523
+ setReadiness(left: success, right: nil)
1508
1524
  } else if peripheral == rightPeripheral {
1509
- rightInitialized = success
1510
- // CoreCommsService.log("G1: Right arm initialized: \(success)")
1511
- setReadiness(left: nil, right: true)
1512
- }
1513
-
1514
- // Only proceed if both glasses are initialized
1515
- if leftInitialized, rightInitialized {
1516
- setReadiness(left: true, right: true)
1525
+ setReadiness(left: nil, right: success)
1517
1526
  }
1518
1527
  }
1519
1528
 
@@ -1733,6 +1742,7 @@ extension G1 {
1733
1742
  }
1734
1743
 
1735
1744
  func getBatteryStatus() {
1745
+ guard !isDisconnecting, leftReady, rightReady else { return }
1736
1746
  Bridge.log("G1: getBatteryStatus()")
1737
1747
  let command: [UInt8] = [Commands.BLE_REQ_BATTERY.rawValue, 0x01]
1738
1748
  queueChunks([command])
@@ -2364,6 +2374,7 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
2364
2374
 
2365
2375
  private func startReconnectionTimer() {
2366
2376
  Task {
2377
+ guard !isDisconnecting else { return }
2367
2378
  await reconnectionManager.start { [weak self] in
2368
2379
  guard let self else { return false }
2369
2380
 
@@ -2377,6 +2388,8 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
2377
2388
 
2378
2389
  // Attempt to reconnect
2379
2390
  await MainActor.run {
2391
+ guard !self.isDisconnecting else { return }
2392
+ self.scanRequested = true
2380
2393
  self.startScan()
2381
2394
  }
2382
2395
 
@@ -2401,17 +2414,22 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
2401
2414
  }
2402
2415
 
2403
2416
  Bridge.log("G1: 🔵 Attempting to connect by UUID")
2404
- var foundAny = false
2417
+ var foundSides = 0
2405
2418
 
2406
2419
  if let leftUUID = leftGlassUUID {
2407
2420
  Bridge.log("G1: 🔵 Found stored left glass UUID: \(leftUUID.uuidString)")
2408
2421
  let leftDevices = centralManager!.retrievePeripherals(withIdentifiers: [leftUUID])
2409
2422
 
2410
- if let leftDevice = leftDevices.first {
2423
+ if let leftDevice = leftDevices.first,
2424
+ G1ConnectionTarget.matchesCachedPeripheral(
2425
+ name: leftDevice.name, searchID: DEVICE_SEARCH_ID,
2426
+ cachedSearchID: UserDefaults.standard.string(forKey: "leftGlassSearchID")
2427
+ )
2428
+ {
2411
2429
  Bridge.log(
2412
2430
  "G1: 🔵 Successfully retrieved left glass: \(leftDevice.name ?? "Unknown")"
2413
2431
  )
2414
- foundAny = true
2432
+ foundSides += 1
2415
2433
  leftPeripheral = leftDevice
2416
2434
  leftDevice.delegate = self
2417
2435
  centralManager!.connect(
@@ -2428,11 +2446,16 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
2428
2446
  Bridge.log("G1: 🔵 Found stored right glass UUID: \(rightUUID.uuidString)")
2429
2447
  let rightDevices = centralManager!.retrievePeripherals(withIdentifiers: [rightUUID])
2430
2448
 
2431
- if let rightDevice = rightDevices.first {
2449
+ if let rightDevice = rightDevices.first,
2450
+ G1ConnectionTarget.matchesCachedPeripheral(
2451
+ name: rightDevice.name, searchID: DEVICE_SEARCH_ID,
2452
+ cachedSearchID: UserDefaults.standard.string(forKey: "rightGlassSearchID")
2453
+ )
2454
+ {
2432
2455
  Bridge.log(
2433
2456
  "G1: 🔵 Successfully retrieved right glass: \(rightDevice.name ?? "Unknown")"
2434
2457
  )
2435
- foundAny = true
2458
+ foundSides += 1
2436
2459
  rightPeripheral = rightDevice
2437
2460
  rightDevice.delegate = self
2438
2461
  centralManager!.connect(
@@ -2445,7 +2468,8 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
2445
2468
  }
2446
2469
  }
2447
2470
 
2448
- return foundAny
2471
+ // Continue scanning if only one matching cached half was available.
2472
+ return foundSides == 2
2449
2473
  }
2450
2474
 
2451
2475
  func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) {
@@ -2502,18 +2526,24 @@ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
2502
2526
  /// called whenever bluetooth is initialized / turned on or off:
2503
2527
  func centralManagerDidUpdateState(_ central: CBCentralManager) {
2504
2528
  DispatchQueue.main.async { [weak self] in
2505
- guard let self = self else { return }
2529
+ self?.handleBluetoothState(central.state)
2530
+ }
2531
+ }
2506
2532
 
2507
- if central.state == .poweredOn {
2508
- Bridge.log("G1: Bluetooth was powered on")
2509
- self.setReadiness(left: false, right: false)
2533
+ func handleBluetoothState(_ state: CBManagerState) {
2534
+ if state == .poweredOn {
2535
+ Bridge.log("G1: Bluetooth was powered on")
2536
+ setReadiness(left: false, right: false)
2510
2537
 
2511
- if self.DEVICE_SEARCH_ID != "NOT_SET", !self.DEVICE_SEARCH_ID.isEmpty {
2512
- self.startScan()
2513
- }
2514
- } else {
2515
- Bridge.log("G1: Bluetooth was turned off.")
2538
+ // Discovery has no search ID, but still needs to resume after the
2539
+ // central manager's initial .unknown state. Preserve selected-pair
2540
+ // reconnects after a power cycle, unless intentionally disconnected.
2541
+ let hasConnectionTarget = DEVICE_SEARCH_ID != "NOT_SET" && !DEVICE_SEARCH_ID.isEmpty
2542
+ if !isDisconnecting, scanRequested || hasConnectionTarget {
2543
+ startScan()
2516
2544
  }
2545
+ } else {
2546
+ Bridge.log("G1: Bluetooth was turned off.")
2517
2547
  }
2518
2548
  }
2519
2549
 
@@ -0,0 +1,13 @@
1
+ import Foundation
2
+
3
+ enum G1ConnectionTarget {
4
+ /// A legacy UUID needs a matching peripheral name; newly recorded UUIDs can
5
+ /// also reconnect without a name because their search ID was saved with them.
6
+ static func matchesCachedPeripheral(name: String?, searchID: String, cachedSearchID: String?) -> Bool {
7
+ guard searchID != "NOT_SET", searchID.count > 2 else { return false }
8
+ if let name, !name.isEmpty {
9
+ return name.contains("G1" + searchID)
10
+ }
11
+ return cachedSearchID == searchID
12
+ }
13
+ }