@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
@@ -34,6 +34,7 @@ import android.os.SystemClock
34
34
  import com.mentra.bluetoothsdk.utils.NativeLog as Log
35
35
  import androidx.core.app.ActivityCompat
36
36
  import com.mentra.bluetoothsdk.BluetoothSdkDefaults
37
+ import com.mentra.bluetoothsdk.BluetoothSdkException
37
38
  import com.mentra.bluetoothsdk.Bridge
38
39
  import com.mentra.bluetoothsdk.DeviceManager
39
40
  import com.mentra.bluetoothsdk.PhotoRequest
@@ -141,6 +142,7 @@ class MentraLive : SGCManager() {
141
142
  private const val LC3_FRAME_SIZE = 40
142
143
  private const val VOICE_ACTIVITY_DETECTION_SWITCH_TYPE = 8
143
144
  private const val LOUDNESS_GATE_SWITCH_TYPE = 10
145
+ private const val AUTO_POWER_OFF_SWITCH_TYPE = 11
144
146
  // Mic tuning field names, matching the BES cs_mictun body.
145
147
  private val MIC_TUNING_FIELDS =
146
148
  listOf("gain", "open", "close", "attack", "hang", "sp_open", "sp_close", "sp_hold")
@@ -644,6 +646,10 @@ class MentraLive : SGCManager() {
644
646
  private var a2dpProfile: BluetoothA2dp? = null
645
647
  private var isA2dpProxyRegistered = false
646
648
  private var a2dpConnectAttempts = 0
649
+ private val classicAudioServiceDiscovery =
650
+ ClassicAudioServiceDiscovery(handler, CONNECTION_TIMEOUT_MS.toLong()) {
651
+ Bridge.log("LIVE: Classic: $it")
652
+ }
647
653
 
648
654
  private data class OutgoingBleCommandTraceInfo(
649
655
  val commandType: String,
@@ -776,7 +782,8 @@ class MentraLive : SGCManager() {
776
782
  }
777
783
 
778
784
  // Inner class to track incoming file transfers
779
- private class FileTransferSession(var fileName: String, var fileSize: Int) {
785
+ internal class FileTransferSession(var fileName: String, var fileSize: Int, flags: Int = 0) {
786
+ private val dynamicPayload = (flags and 0x0002) != 0
780
787
  // NOTE: fileSize may be "fake" (inflated) due to BES firmware workaround
781
788
  var actualPackSize =
782
789
  0 // Actual pack size from first received packet (for BES lie detection)
@@ -805,9 +812,8 @@ class MentraLive : SGCManager() {
805
812
  * Recalculate total packets based on actual pack size from received packet. Called when
806
813
  * first packet is received to handle variable pack sizes.
807
814
  *
808
- * NOTE: Due to BES firmware workaround, fileSize in header may be "fake" (inflated). We
809
- * detect this by checking if fileSize is a multiple of 400 (BES_HARDCODED_PACK_SIZE). If
810
- * so, totalPackets = fileSize / 400, regardless of actual pack size.
815
+ * Dynamic-payload headers carry the true byte count. Only legacy small packs use
816
+ * the inflated-size workaround (multiples of BES_HARDCODED_PACK_SIZE).
811
817
  */
812
818
  fun recalculateTotalPackets(actualPackSize: Int) {
813
819
  if (actualPackSize <= 0 || actualPackSize > K900ProtocolUtils.FILE_PACK_SIZE) {
@@ -818,8 +824,8 @@ class MentraLive : SGCManager() {
818
824
 
819
825
  // Detect BES lie: if fileSize is exact multiple of 400, glasses used the lie strategy
820
826
  val isBesLie =
821
- (fileSize % BES_HARDCODED_PACK_SIZE == 0) &&
822
- (actualPackSize != BES_HARDCODED_PACK_SIZE)
827
+ !dynamicPayload && (fileSize % BES_HARDCODED_PACK_SIZE == 0) &&
828
+ (actualPackSize < BES_HARDCODED_PACK_SIZE)
823
829
 
824
830
  val newTotalPackets: Int
825
831
  if (isBesLie) {
@@ -857,6 +863,10 @@ class MentraLive : SGCManager() {
857
863
  }
858
864
 
859
865
  fun addPacket(index: Int, data: ByteArray): Boolean {
866
+ if (dynamicPayload && (index < 0 || index >= totalPackets || actualPackSize <= 0 ||
867
+ data.size != minOf(actualPackSize, fileSize - index * actualPackSize))) {
868
+ return false
869
+ }
860
870
  if (index >= 0 && index < totalPackets && !receivedPackets.containsKey(index)) {
861
871
  receivedPackets[index] = data
862
872
 
@@ -1703,6 +1713,7 @@ class MentraLive : SGCManager() {
1703
1713
  readinessCompletedThisBleSession = false
1704
1714
  glassesReadyReceived = false
1705
1715
  ctkdInitiatedThisGattSession = false
1716
+ classicAudioServiceDiscovery.cancel()
1706
1717
  audioConnected = false
1707
1718
  notificationsEnabled = false
1708
1719
  currentMtu = 23
@@ -1978,6 +1989,7 @@ class MentraLive : SGCManager() {
1978
1989
  readinessCompletedThisBleSession = false
1979
1990
  glassesReadyReceived = false
1980
1991
  ctkdInitiatedThisGattSession = false
1992
+ classicAudioServiceDiscovery.cancel()
1981
1993
  audioConnected = false
1982
1994
  notificationsEnabled = false
1983
1995
  processSendQueueRunnable?.let { handler.removeCallbacks(it) }
@@ -2147,7 +2159,7 @@ class MentraLive : SGCManager() {
2147
2159
  // than scanning).
2148
2160
  // Falls back to name-based scan if no address is saved.
2149
2161
  val lastDeviceAddress =
2150
- DeviceStore.get("bluetooth", "device_address") as String?
2162
+ selectedConnectionAddress()
2151
2163
  if (lastDeviceAddress != null &&
2152
2164
  !lastDeviceAddress.isEmpty() &&
2153
2165
  bluetoothAdapter != null
@@ -2345,6 +2357,7 @@ class MentraLive : SGCManager() {
2345
2357
  registerBondingReceiver()
2346
2358
  bondingRetryCount = 0
2347
2359
  ctkdInitiatedThisGattSession = false
2360
+ classicAudioServiceDiscovery.cancel()
2348
2361
  Bridge.log(
2349
2362
  "LIVE: CTKD: BLE GATT up - deferring Classic/A2DP until notifications ready"
2350
2363
  )
@@ -2393,6 +2406,7 @@ class MentraLive : SGCManager() {
2393
2406
  // Reset audio pairing flags
2394
2407
  glassesReadyReceived = false
2395
2408
  ctkdInitiatedThisGattSession = false
2409
+ classicAudioServiceDiscovery.cancel()
2396
2410
  audioConnected = false
2397
2411
 
2398
2412
  notificationsEnabled = false
@@ -2455,6 +2469,7 @@ class MentraLive : SGCManager() {
2455
2469
  glassesReadyReceived = false
2456
2470
  audioConnected = false
2457
2471
  ctkdInitiatedThisGattSession = false
2472
+ classicAudioServiceDiscovery.cancel()
2458
2473
 
2459
2474
  notificationsEnabled = false
2460
2475
 
@@ -3366,10 +3381,12 @@ class MentraLive : SGCManager() {
3366
3381
  sendJson(json, wakeup, true)
3367
3382
  }
3368
3383
 
3369
- private fun sendJson(json: JSONObject?, wakeup: Boolean, bridgeLogging: Boolean): Boolean {
3384
+ private fun sendJson(
3385
+ json: JSONObject?, wakeup: Boolean, bridgeLogging: Boolean,
3386
+ sessionGeneration: Long = bleSessionGeneration.get(),
3387
+ ): Boolean {
3370
3388
  if (json != null) {
3371
3389
  try {
3372
- val sessionGeneration = bleSessionGeneration.get()
3373
3390
  if (buildNumberInt < 5) {
3374
3391
  val jsonStr = json.toString()
3375
3392
  // Bridge.log("LIVE: 📤 Sending JSON with esoteric message ID: " + jsonStr);
@@ -4137,7 +4154,9 @@ class MentraLive : SGCManager() {
4137
4154
  "controllerId" to json.opt("controllerId"), "streamId" to json.opt("streamId"),
4138
4155
  "probeId" to json.opt("probeId"))
4139
4156
  com.mentra.bluetoothsdk.streaming.StreamControllerProbe.response(values)?.let {
4140
- sendJson(JSONObject(it))
4157
+ // BES buffers non-waking commands when MTK enters standby, even while
4158
+ // its streaming CPU lease is held. The current probe needs a live reply.
4159
+ sendJson(JSONObject(it), true)
4141
4160
  }
4142
4161
  }
4143
4162
  "pong" ->
@@ -5548,7 +5567,9 @@ class MentraLive : SGCManager() {
5548
5567
  if (bodyObj != null) {
5549
5568
  val type = bodyObj.optInt("type", -1)
5550
5569
  val value = bodyObj.optInt("switch", -1)
5551
- handleSwitchStatus(type, value, System.currentTimeMillis())
5570
+ // K900 replies carry result in "S"; 0 is RC_SUCCESS.
5571
+ val resultCode = json.optInt("S", -1)
5572
+ handleSwitchStatus(type, value, System.currentTimeMillis(), resultCode)
5552
5573
  }
5553
5574
  } catch (e: Exception) {
5554
5575
  Log.e(TAG, "Error parsing sr_swit response", e)
@@ -5578,7 +5599,13 @@ class MentraLive : SGCManager() {
5578
5599
  try {
5579
5600
  val bodyObj = optK900Body(json)
5580
5601
  if (bodyObj != null) {
5581
- Bridge.sendWearState(bodyObj.optInt("on", 0) != 0)
5602
+ val extras = HashMap<String, Any>()
5603
+ if (bodyObj.has("elapsed_ms")) extras["elapsedMs"] = bodyObj.optInt("elapsed_ms")
5604
+ if (bodyObj.has("timeout_ms")) extras["timeoutMs"] = bodyObj.optInt("timeout_ms")
5605
+ if (bodyObj.has("enabled")) extras["enabled"] = bodyObj.optInt("enabled") != 0
5606
+ if (bodyObj.has("armed")) extras["armed"] = bodyObj.optInt("armed") != 0
5607
+ if (bodyObj.has("inhibited")) extras["inhibited"] = bodyObj.optInt("inhibited") != 0
5608
+ Bridge.sendWearState(bodyObj.optInt("on", 0) != 0, extras)
5582
5609
  }
5583
5610
  } catch (e: Exception) {
5584
5611
  Log.e(TAG, "Error parsing sr_wrst response", e)
@@ -5933,8 +5960,20 @@ class MentraLive : SGCManager() {
5933
5960
  Bridge.sendWearTuningState(state)
5934
5961
  }
5935
5962
 
5936
- private fun handleSwitchStatus(switchType: Int, switchValue: Int, timestamp: Long) {
5963
+ private fun handleSwitchStatus(
5964
+ switchType: Int,
5965
+ switchValue: Int,
5966
+ timestamp: Long,
5967
+ resultCode: Int = -1
5968
+ ) {
5937
5969
  Bridge.sendSwitchStatus(switchType, switchValue, timestamp)
5970
+ if (switchType == AUTO_POWER_OFF_SWITCH_TYPE) {
5971
+ val ok = resultCode == 0
5972
+ Bridge.log(
5973
+ "LIVE: 🔋 auto power-off sr_swit reply type=$switchType switch=$switchValue" +
5974
+ " result=$resultCode ok=$ok"
5975
+ )
5976
+ }
5938
5977
  if (switchType == VOICE_ACTIVITY_DETECTION_SWITCH_TYPE &&
5939
5978
  (switchValue == 0 || switchValue == 1)
5940
5979
  ) {
@@ -6048,6 +6087,17 @@ class MentraLive : SGCManager() {
6048
6087
  }
6049
6088
 
6050
6089
  override fun sendIncidentId(incidentId: String, apiBaseUrl: String?) {
6090
+ if (!isConnected || bluetoothGatt == null || txCharacteristic == null) {
6091
+ throw BluetoothSdkException(
6092
+ "glasses_not_connected", "Cannot request incident logs because the glasses BLE link is not ready.",
6093
+ )
6094
+ }
6095
+ // The JS token sync may happen after glasses_ready. Use only its current native store value.
6096
+ val coreToken = DeviceStore.get("bluetooth", "core_token") as? String
6097
+ if (coreToken.isNullOrBlank()) {
6098
+ throw BluetoothSdkException("core_token_unavailable", "Cannot request incident logs without the current Core access token.")
6099
+ }
6100
+ val sessionGeneration = bleSessionGeneration.get()
6051
6101
  try {
6052
6102
  var base = if (apiBaseUrl != null) apiBaseUrl.trim() else ""
6053
6103
  if (base.isEmpty()) {
@@ -6060,13 +6110,25 @@ class MentraLive : SGCManager() {
6060
6110
  bleIncidentLogRelays[lKey] =
6061
6111
  BleIncidentLogRelay(lKey, incidentId, base, BleIncidentLogKind.LOGCAT)
6062
6112
 
6113
+ val auth = JSONObject()
6114
+ auth.put("type", "auth_token")
6115
+ auth.put("coreToken", coreToken)
6116
+ auth.put("timestamp", System.currentTimeMillis())
6063
6117
  val json = JSONObject()
6064
6118
  json.put("type", "upload_incident_logs")
6065
6119
  json.put("incidentId", incidentId)
6066
6120
  json.put("apiBaseUrl", base)
6067
- sendJson(json, true)
6121
+ // Wake ASG with its current token before the upload command enters the same FIFO.
6122
+ if (!sendJson(auth, true, true, sessionGeneration) ||
6123
+ sessionGeneration != bleSessionGeneration.get() ||
6124
+ !isConnected || bluetoothGatt == null || txCharacteristic == null ||
6125
+ !sendJson(json, true, true, sessionGeneration) || sessionGeneration != bleSessionGeneration.get()) {
6126
+ bleIncidentLogRelays.remove(bKey)
6127
+ bleIncidentLogRelays.remove(lKey)
6128
+ throw BluetoothSdkException("incident_dispatch_failed", "Could not queue the glasses incident-log request.")
6129
+ }
6068
6130
  Bridge.log(
6069
- "LIVE: Sent incidentId to glasses for log upload: " +
6131
+ "LIVE: Queued incidentId to glasses for log upload: " +
6070
6132
  incidentId +
6071
6133
  " (BLE relay keys " +
6072
6134
  bKey +
@@ -6075,7 +6137,7 @@ class MentraLive : SGCManager() {
6075
6137
  ")"
6076
6138
  )
6077
6139
  } catch (e: JSONException) {
6078
- Log.e(TAG, "Error creating upload_incident_logs command", e)
6140
+ throw BluetoothSdkException("incident_dispatch_failed", "Could not create the glasses incident-log request.", e)
6079
6141
  }
6080
6142
  }
6081
6143
 
@@ -6140,7 +6202,8 @@ class MentraLive : SGCManager() {
6140
6202
  val json = JSONObject()
6141
6203
  json.put("type", "request_version")
6142
6204
  requestId?.let { json.put("request_id", it) }
6143
- sendJson(json, false)
6205
+ // Wake ASG so the version request and its response can finish after idle.
6206
+ sendJson(json, true)
6144
6207
  Bridge.log("LIVE: 📱 Requesting version info from glasses")
6145
6208
  } catch (e: JSONException) {
6146
6209
  Log.e(TAG, "📱 Error creating request_version command", e)
@@ -6602,6 +6665,14 @@ class MentraLive : SGCManager() {
6602
6665
  return false
6603
6666
  }
6604
6667
 
6668
+ private fun selectedConnectionAddress(): String? = SelectedDeviceAddress.resolve(
6669
+ savedDeviceName,
6670
+ DeviceStore.get("bluetooth", "pending_device_name") as? String,
6671
+ DeviceStore.get("bluetooth", "pending_device_address") as? String,
6672
+ DeviceStore.get("bluetooth", "device_name") as? String,
6673
+ DeviceStore.get("bluetooth", "device_address") as? String,
6674
+ )
6675
+
6605
6676
  fun connectToSmartGlasses() {
6606
6677
  if (postGattLifecycle { connectToSmartGlasses() }) return
6607
6678
  if (pairingYieldActive) {
@@ -6636,13 +6707,7 @@ class MentraLive : SGCManager() {
6636
6707
  // var context = Bridge.getContext();
6637
6708
  // SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
6638
6709
  // String lastDeviceAddress = prefs.getString(PREF_DEVICE_NAME, null);
6639
- val pendingAddress =
6640
- (DeviceStore.get("bluetooth", "pending_device_address") as String?)?.takeIf {
6641
- it.isNotEmpty()
6642
- }
6643
- val lastDeviceAddress =
6644
- pendingAddress
6645
- ?: (DeviceStore.get("bluetooth", "device_address") as String?)
6710
+ val lastDeviceAddress = selectedConnectionAddress()
6646
6711
 
6647
6712
  if (lastDeviceAddress != null && lastDeviceAddress.length > 0) {
6648
6713
  // Connect to last known device if available
@@ -7178,7 +7243,7 @@ class MentraLive : SGCManager() {
7178
7243
  BluetoothDevice.BOND_BONDED ->
7179
7244
  run {
7180
7245
  Bridge.log(
7181
- "LIVE: CTKD: ✅ Successfully bonded with device - BT Classic connection established"
7246
+ "LIVE: CTKD: Successfully bonded - checking Classic audio services"
7182
7247
  )
7183
7248
  markPairingTiming(
7184
7249
  "ctkd_bonded",
@@ -7197,7 +7262,7 @@ class MentraLive : SGCManager() {
7197
7262
  // path does this at GATT connect).
7198
7263
  // markAudioConnected / sendAudioConnected fire
7199
7264
  // when the A2DP path completes.
7200
- connectA2dpProfile(device)
7265
+ prepareClassicAudio(device)
7201
7266
 
7202
7267
  // Safety net: if glasses_ready somehow missed
7203
7268
  // setting fullyBooted, catch up here.
@@ -7234,6 +7299,7 @@ class MentraLive : SGCManager() {
7234
7299
  }
7235
7300
  }
7236
7301
  BluetoothDevice.BOND_NONE -> {
7302
+ classicAudioServiceDiscovery.cancel()
7237
7303
  Bridge.log(
7238
7304
  "LIVE: CTKD: ❌ Bonding failed or removed for device"
7239
7305
  )
@@ -7355,6 +7421,10 @@ class MentraLive : SGCManager() {
7355
7421
  classicAudioReceiver =
7356
7422
  object : BroadcastReceiver() {
7357
7423
  override fun onReceive(context: Context?, intent: Intent?) {
7424
+ if (intent?.action == BluetoothDevice.ACTION_UUID) {
7425
+ intent.bluetoothDeviceExtra()?.let { classicAudioServiceDiscovery.onUuids(it) }
7426
+ return
7427
+ }
7358
7428
  if (intent?.action != BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED &&
7359
7429
  intent?.action != BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) return
7360
7430
  val device = intent.bluetoothDeviceExtra() ?: return
@@ -7371,6 +7441,7 @@ class MentraLive : SGCManager() {
7371
7441
  try {
7372
7442
  val filter =
7373
7443
  IntentFilter().apply {
7444
+ addAction(BluetoothDevice.ACTION_UUID)
7374
7445
  addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)
7375
7446
  addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
7376
7447
  }
@@ -7495,10 +7566,10 @@ class MentraLive : SGCManager() {
7495
7566
  when (device.bondState) {
7496
7567
  BluetoothDevice.BOND_BONDED -> {
7497
7568
  Bridge.log(
7498
- "LIVE: CTKD: Device is already bonded - connecting A2DP audio profile"
7569
+ "LIVE: CTKD: Device is already bonded - checking Classic audio services"
7499
7570
  )
7500
7571
  markPairingTiming("ctkd_already_bonded")
7501
- connectA2dpProfile(device)
7572
+ prepareClassicAudio(device)
7502
7573
  }
7503
7574
  BluetoothDevice.BOND_BONDING -> {
7504
7575
  // Secure pairing firmware may request SMP as soon as GATT connects.
@@ -7557,6 +7628,19 @@ class MentraLive : SGCManager() {
7557
7628
  return classicAudioConnectionTracker.connected
7558
7629
  }
7559
7630
 
7631
+ private fun prepareClassicAudio(device: BluetoothDevice) {
7632
+ if (!canConnectClassicAudio(device)) return
7633
+ val gatt = bluetoothGatt
7634
+ classicAudioServiceDiscovery.connectWhenDiscovered(device) {
7635
+ if (bluetoothGatt === gatt && canConnectClassicAudio(device)) connectA2dpProfile(device)
7636
+ }
7637
+ }
7638
+
7639
+ private fun canConnectClassicAudio(device: BluetoothDevice): Boolean =
7640
+ !isKilled && !pairingYieldActive && isConnected && notificationsEnabled &&
7641
+ ctkdInitiatedThisGattSession && connectedDevice?.address == device.address &&
7642
+ device.bondState == BluetoothDevice.BOND_BONDED
7643
+
7560
7644
  /** A2DP profile service listener for connecting to already-bonded devices */
7561
7645
  private val a2dpServiceListener: BluetoothProfile.ServiceListener =
7562
7646
  object : BluetoothProfile.ServiceListener {
@@ -7605,7 +7689,9 @@ class MentraLive : SGCManager() {
7605
7689
  Bridge.log("LIVE: A2DP: Skipping connectA2dpWithProxy — SGC destroyed")
7606
7690
  return
7607
7691
  }
7608
- if (a2dpProfile == null || device == null) {
7692
+ if (device == null || !canConnectClassicAudio(device) ||
7693
+ !ClassicAudioServiceDiscovery.hasAudioSink(device)) return
7694
+ if (a2dpProfile == null) {
7609
7695
  Bridge.log("LIVE: A2DP: Cannot connect - proxy or device is null")
7610
7696
  return
7611
7697
  }
@@ -7655,14 +7741,11 @@ class MentraLive : SGCManager() {
7655
7741
  "LIVE: A2DP: Retry $a2dpConnectAttempts/$A2DP_CONNECT_MAX_ATTEMPTS in " +
7656
7742
  "${A2DP_CONNECT_RETRY_MS}ms ($reason)"
7657
7743
  )
7744
+ val gatt = bluetoothGatt
7658
7745
  handler.postDelayed(
7659
7746
  Runnable {
7660
- if (isKilled || pairingYieldActive || connectedDevice == null) {
7661
- return@Runnable
7662
- }
7663
- val target = connectedDevice ?: return@Runnable
7664
- if (a2dpProfile != null) {
7665
- connectA2dpWithProxy(target)
7747
+ if (bluetoothGatt === gatt && canConnectClassicAudio(device)) {
7748
+ connectA2dpWithProxy(device)
7666
7749
  }
7667
7750
  },
7668
7751
  A2DP_CONNECT_RETRY_MS
@@ -7882,6 +7965,7 @@ class MentraLive : SGCManager() {
7882
7965
 
7883
7966
  // Mark as killed to prevent reconnection attempts
7884
7967
  isKilled = true
7968
+ classicAudioServiceDiscovery.cancel()
7885
7969
  explicitConnectByName = false
7886
7970
  pairingYieldActive = false
7887
7971
  pairingYieldAwaitingReclaim = false
@@ -7903,7 +7987,11 @@ class MentraLive : SGCManager() {
7903
7987
 
7904
7988
  // Tear down Classic (A2DP/HFP) before GATT close. Closing only the proxy left the
7905
7989
  // phone's system Bluetooth UI showing Mentra Live still connected.
7906
- val classicDevice = connectedDevice
7990
+ // BLE loss clears connectedDevice while the OS audio profiles can remain connected.
7991
+ // Use this session's existing Classic target before resetting the tracker below.
7992
+ val classicDevice = connectedDevice ?: classicAudioConnectionTracker.targetForDisconnect()?.let {
7993
+ bluetoothAdapter?.getRemoteDevice(it)
7994
+ }
7907
7995
  disconnectClassicProfiles(classicDevice)
7908
7996
  closeA2dpProxy()
7909
7997
  classicAudioConnectionTracker.reset()
@@ -7980,6 +8068,7 @@ class MentraLive : SGCManager() {
7980
8068
  glassesReadyReceived = false
7981
8069
  audioConnected = false
7982
8070
  ctkdInitiatedThisGattSession = false
8071
+ classicAudioServiceDiscovery.cancel()
7983
8072
  DeviceStore.apply("glasses", "fullyBooted", false)
7984
8073
  updateConnectionState(ConnTypes.DISCONNECTED)
7985
8074
 
@@ -9467,7 +9556,11 @@ class MentraLive : SGCManager() {
9467
9556
  }
9468
9557
  } else {
9469
9558
  // Normal single message transmission
9470
- transportLog("LIVE: Sending data to glasses: " + wireData, bridgeLogging)
9559
+ transportLog(
9560
+ "LIVE: Sending data to glasses: " +
9561
+ loggableOutgoingPayload(wireData, commandTraceInfo.commandType),
9562
+ bridgeLogging
9563
+ )
9471
9564
 
9472
9565
  // Pack the data using the centralized utility with the negotiated endianness
9473
9566
  val packedData =
@@ -9769,6 +9862,18 @@ class MentraLive : SGCManager() {
9769
9862
  }
9770
9863
  }
9771
9864
 
9865
+ /**
9866
+ * Credentials are sent unchanged but never logged; the BLE trace redacts their fields.
9867
+ */
9868
+ private fun loggableOutgoingPayload(payload: String, commandType: String): String =
9869
+ try {
9870
+ val json = JSONObject(payload)
9871
+ if (json.has("password") || json.has("coreToken")) "<$commandType with credentials omitted>"
9872
+ else payload
9873
+ } catch (_: JSONException) {
9874
+ payload
9875
+ }
9876
+
9772
9877
  private fun summarizeOutgoingMessage(payload: String?): String {
9773
9878
  if (payload == null || payload.isEmpty()) {
9774
9879
  return "type=unknown, requestId=none, appId=none, mode=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, iso=none, mId=none"
@@ -10193,7 +10298,7 @@ class MentraLive : SGCManager() {
10193
10298
  if (incidentRelay.session == null) {
10194
10299
  activeFileTransfers.remove(packetInfo.fileName)
10195
10300
  incidentRelay.session =
10196
- FileTransferSession(packetInfo.fileName, packetInfo.fileSize)
10301
+ FileTransferSession(packetInfo.fileName, packetInfo.fileSize, packetInfo.flags)
10197
10302
  incidentRelay.session!!.recalculateTotalPackets(packetInfo.packSize)
10198
10303
  Bridge.log(
10199
10304
  "LIVE: 📦 Started BLE incident log transfer: " +
@@ -10244,7 +10349,7 @@ class MentraLive : SGCManager() {
10244
10349
  // Get or create session for this transfer
10245
10350
  if (photoTransfer.session == null) {
10246
10351
  photoTransfer.session =
10247
- FileTransferSession(packetInfo.fileName, packetInfo.fileSize)
10352
+ FileTransferSession(packetInfo.fileName, packetInfo.fileSize, packetInfo.flags)
10248
10353
  // Recalculate total packets based on actual pack size (handles variable MTU)
10249
10354
  photoTransfer.session!!.recalculateTotalPackets(packetInfo.packSize)
10250
10355
  Bridge.log(
@@ -10330,7 +10435,7 @@ class MentraLive : SGCManager() {
10330
10435
  var session = activeFileTransfers[packetInfo.fileName]
10331
10436
  if (session == null) {
10332
10437
  // New file transfer
10333
- session = FileTransferSession(packetInfo.fileName, packetInfo.fileSize)
10438
+ session = FileTransferSession(packetInfo.fileName, packetInfo.fileSize, packetInfo.flags)
10334
10439
  // Recalculate total packets based on actual pack size (handles variable MTU)
10335
10440
  session.recalculateTotalPackets(packetInfo.packSize)
10336
10441
  activeFileTransfers[packetInfo.fileName] = session
@@ -10825,6 +10930,9 @@ class MentraLive : SGCManager() {
10825
10930
  // Send glasses-side loudness / Barrier gate setting.
10826
10931
  sendLoudnessGateSetting()
10827
10932
 
10933
+ // Send glasses-side auto power-off setting.
10934
+ sendAutoPowerOffSetting()
10935
+
10828
10936
  // Send mic tuning. With nothing authorized this sends a reset, which is
10829
10937
  // what returns a freshly connected pair of glasses to stock behaviour.
10830
10938
  sendMicTuningSetting()
@@ -11108,6 +11216,52 @@ class MentraLive : SGCManager() {
11108
11216
  }
11109
11217
  }
11110
11218
 
11219
+ override fun sendAutoPowerOffSetting() {
11220
+ val value = DeviceStore.get("bluetooth", "auto_power_off_enabled")
11221
+ val enabled =
11222
+ if (value is Boolean) value
11223
+ else BluetoothSdkDefaults.AUTO_POWER_OFF_ENABLED
11224
+
11225
+ Bridge.log(
11226
+ "LIVE: 🔋 Sending auto power-off setting to glasses: enabled=$enabled" +
11227
+ " (cs_swit type=$AUTO_POWER_OFF_SWITCH_TYPE)"
11228
+ )
11229
+
11230
+ if (!isConnected) {
11231
+ Bridge.log("LIVE: Cannot send auto power-off setting - not connected")
11232
+ return
11233
+ }
11234
+
11235
+ try {
11236
+ val body = JSONObject()
11237
+ body.put("type", AUTO_POWER_OFF_SWITCH_TYPE)
11238
+ body.put("switch", if (enabled) 1 else 0)
11239
+
11240
+ val cmdObject = JSONObject()
11241
+ cmdObject.put("C", "cs_swit")
11242
+ cmdObject.put("V", 1)
11243
+ cmdObject.put("B", body.toString())
11244
+
11245
+ val packedData =
11246
+ K900ProtocolUtils.packDataToK900(
11247
+ cmdObject.toString().toByteArray(StandardCharsets.UTF_8),
11248
+ K900ProtocolUtils.CMD_TYPE_STRING,
11249
+ k900LengthEndian()
11250
+ )
11251
+ if (packedData == null) {
11252
+ Bridge.log("LIVE: Failed to pack auto power-off setting command")
11253
+ return
11254
+ }
11255
+ queueData(packedData)
11256
+ Bridge.log(
11257
+ "LIVE: 🔋 Queued auto power-off cs_swit type=$AUTO_POWER_OFF_SWITCH_TYPE" +
11258
+ " switch=${if (enabled) 1 else 0}"
11259
+ )
11260
+ } catch (e: JSONException) {
11261
+ Log.e(TAG, "Error creating auto power-off setting command", e)
11262
+ }
11263
+ }
11264
+
11111
11265
  /** Send button photo settings to glasses, replaying all stored scan-tuning fields. */
11112
11266
  override fun sendButtonPhotoSettings() {
11113
11267
  val legacyZslMfnr =
@@ -208,7 +208,7 @@ class MentraNex : SGCManager() {
208
208
 
209
209
  // If we already have saved device names for main...
210
210
  if (name != null && preferredMainDeviceId != null) {
211
- if (!name.contains(preferredMainDeviceId!!)) {
211
+ if (name != preferredMainDeviceId) {
212
212
  return // Not a matching device
213
213
  }
214
214
  }
@@ -230,7 +230,6 @@ class MentraNex : SGCManager() {
230
230
  type = DeviceTypes.NEX
231
231
  hasMic = true
232
232
  DeviceStore.apply("glasses", "micEnabled", false)
233
- preferredMainDeviceId = DeviceManager.getInstance().deviceName
234
233
 
235
234
  // Initialize LC3 audio player
236
235
  lc3AudioPlayer = Lc3Player(context)
@@ -1315,23 +1314,28 @@ class MentraNex : SGCManager() {
1315
1314
  }
1316
1315
 
1317
1316
  private fun connectToSmartGlasses() {
1318
- val deviceModelName = DeviceManager.getInstance().deviceName
1319
- val deviceAddress = DeviceManager.getInstance().deviceAddress
1317
+ val target = preferredMainDeviceId?.takeIf { it.isNotBlank() } ?: return
1318
+ val deviceAddress = SelectedDeviceAddress.resolve(
1319
+ target,
1320
+ DeviceStore.get("bluetooth", "pending_device_name") as? String,
1321
+ DeviceStore.get("bluetooth", "pending_device_address") as? String,
1322
+ DeviceStore.get("bluetooth", "device_name") as? String,
1323
+ DeviceStore.get("bluetooth", "device_address") as? String,
1324
+ )
1320
1325
 
1321
1326
  // Register bonding receiver
1322
1327
  Bridge.log("connectToSmartGlasses start")
1323
- Bridge.log("try to ConnectToSmartGlassesing deviceModelName: ${deviceModelName} deviceAddress: ${deviceAddress}")
1324
- preferredMainDeviceId = DeviceManager.getInstance().deviceName
1328
+ Bridge.log("Connecting to selected Nex: $target address: $deviceAddress")
1325
1329
  if (!bluetoothAdapter.isEnabled) {
1326
1330
  return
1327
1331
  }
1328
1332
  when {
1329
- !deviceModelName.isNullOrEmpty() && !deviceAddress.isNullOrEmpty() -> {
1333
+ deviceAddress != null -> {
1330
1334
  stopScan()
1331
1335
  mainDevice = bluetoothAdapter.getRemoteDevice(deviceAddress)
1332
1336
  mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 0)
1333
1337
  }
1334
- savedNexMainAddress != null -> {
1338
+ savedNexMainName == target && savedNexMainAddress != null -> {
1335
1339
  mainDevice = bluetoothAdapter.getRemoteDevice(savedNexMainAddress)
1336
1340
  mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 0)
1337
1341
  }