@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-beta.103

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 (140) hide show
  1. package/README.md +67 -25
  2. package/android/build.gradle +7 -4
  3. package/android/lc3Lib/build.gradle +18 -4
  4. package/android/silero/build.gradle +9 -1
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +35 -4
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +205 -19
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +7 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +127 -8
  12. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
  13. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
  14. package/android/src/main/java/com/mentra/bluetoothsdk/ReleaseChangelog.kt +61 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
  22. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1186 -191
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
  29. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
  30. package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  36. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
  37. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
  38. package/build/BluetoothSdk.types.d.ts +82 -2
  39. package/build/BluetoothSdk.types.d.ts.map +1 -1
  40. package/build/BluetoothSdk.types.js.map +1 -1
  41. package/build/_internal.d.ts +2 -0
  42. package/build/_internal.d.ts.map +1 -1
  43. package/build/_internal.js +2 -0
  44. package/build/_internal.js.map +1 -1
  45. package/build/_private/BluetoothSdkModule.d.ts +4 -1
  46. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  47. package/build/_private/BluetoothSdkModule.js +1 -1
  48. package/build/_private/BluetoothSdkModule.js.map +1 -1
  49. package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
  50. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  51. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  52. package/build/_private/sdkOtaManifest.d.ts +3 -12
  53. package/build/_private/sdkOtaManifest.d.ts.map +1 -1
  54. package/build/_private/sdkOtaManifest.js +5 -17
  55. package/build/_private/sdkOtaManifest.js.map +1 -1
  56. package/build/changelogs.d.ts +7 -0
  57. package/build/changelogs.d.ts.map +1 -0
  58. package/build/changelogs.js +44 -0
  59. package/build/changelogs.js.map +1 -0
  60. package/build/generated/changelogCatalog.d.ts +6 -0
  61. package/build/generated/changelogCatalog.d.ts.map +1 -0
  62. package/build/generated/changelogCatalog.js +8 -0
  63. package/build/generated/changelogCatalog.js.map +1 -0
  64. package/build/generated/releaseMetadata.d.ts +12 -0
  65. package/build/generated/releaseMetadata.d.ts.map +1 -0
  66. package/build/generated/releaseMetadata.js +10 -0
  67. package/build/generated/releaseMetadata.js.map +1 -0
  68. package/build/index.d.ts +1 -1
  69. package/build/index.d.ts.map +1 -1
  70. package/build/index.js +24 -2
  71. package/build/index.js.map +1 -1
  72. package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
  73. package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
  74. package/build/ota-server/MentraOtaServer.types.js +2 -0
  75. package/build/ota-server/MentraOtaServer.types.js.map +1 -0
  76. package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
  77. package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
  78. package/build/ota-server/MentraOtaServerModule.js +3 -0
  79. package/build/ota-server/MentraOtaServerModule.js.map +1 -0
  80. package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
  81. package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
  82. package/build/ota-server/MentraOtaServerModule.web.js +12 -0
  83. package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
  84. package/build/ota-server/index.d.ts +3 -0
  85. package/build/ota-server/index.d.ts.map +1 -0
  86. package/build/ota-server/index.js +3 -0
  87. package/build/ota-server/index.js.map +1 -0
  88. package/build/ota-transport/index.d.ts +67 -0
  89. package/build/ota-transport/index.d.ts.map +1 -0
  90. package/build/ota-transport/index.js +58 -0
  91. package/build/ota-transport/index.js.map +1 -0
  92. package/expo-module.config.json +3 -2
  93. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  94. package/ios/BluetoothSdkModule.swift +16 -2
  95. package/ios/LocalIPv4.swift +150 -0
  96. package/ios/LocalOtaServer.swift +309 -0
  97. package/ios/MentraBluetoothSDK.podspec +5 -1
  98. package/ios/MentraOtaServerModule.swift +147 -0
  99. package/ios/MentraPhotoReceiverModule.swift +1 -81
  100. package/ios/Source/BluetoothSdkDefaults.swift +4 -1
  101. package/ios/Source/Bridge.swift +43 -4
  102. package/ios/Source/DeviceManager.swift +143 -13
  103. package/ios/Source/DeviceStore.swift +2 -0
  104. package/ios/Source/GeneratedChangelogCatalog.swift +6 -0
  105. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  106. package/ios/Source/MentraBluetoothSDK.swift +91 -6
  107. package/ios/Source/OtaManifest.swift +5 -7
  108. package/ios/Source/ReleaseChangelog.swift +65 -0
  109. package/ios/Source/audio/AudioModels.swift +17 -0
  110. package/ios/Source/camera/CameraModels.swift +1 -0
  111. package/ios/Source/events/BluetoothEvents.swift +27 -0
  112. package/ios/Source/sgcs/MentraLive.swift +453 -31
  113. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
  114. package/ios/Source/sgcs/SGCManager.swift +11 -0
  115. package/ios/Source/status/DeviceStatus.swift +23 -0
  116. package/ios/Source/types/DeviceModels.swift +23 -2
  117. package/ios/Source/utils/BleJsonCompact.swift +73 -71
  118. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
  119. package/ios/Tests/ReleaseChangelogTests.swift +13 -0
  120. package/package.json +10 -1
  121. package/plugin/build/withIos.d.ts +3 -1
  122. package/plugin/build/withIos.js +22 -10
  123. package/scripts/public-ota-api.test.mjs +37 -0
  124. package/scripts/verify-release-package.mjs +34 -0
  125. package/scripts/write-release-metadata.mjs +154 -0
  126. package/scripts/write-release-metadata.test.mjs +29 -0
  127. package/src/BluetoothSdk.types.ts +95 -3
  128. package/src/_internal.ts +2 -0
  129. package/src/_private/BluetoothSdkModule.ts +5 -5
  130. package/src/_private/MentraLocalNetworkModule.ts +1 -1
  131. package/src/_private/sdkOtaManifest.ts +5 -17
  132. package/src/changelogs.ts +44 -0
  133. package/src/generated/changelogCatalog.ts +9 -0
  134. package/src/generated/releaseMetadata.ts +20 -0
  135. package/src/index.ts +43 -2
  136. package/src/ota-server/MentraOtaServer.types.ts +10 -0
  137. package/src/ota-server/MentraOtaServerModule.ts +23 -0
  138. package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
  139. package/src/ota-server/index.ts +2 -0
  140. package/src/ota-transport/index.ts +116 -0
@@ -25,6 +25,35 @@ struct MentraLiveDevice {
25
25
  let address: String
26
26
  }
27
27
 
28
+ enum MentraLivePendingPairingTarget {
29
+ static func matches(
30
+ connectedName: String,
31
+ connectedIdentifier: String,
32
+ pendingName: String,
33
+ pendingIdentifier: String
34
+ ) -> Bool {
35
+ if !pendingIdentifier.isEmpty {
36
+ return connectedIdentifier.caseInsensitiveCompare(pendingIdentifier) == .orderedSame
37
+ }
38
+ return !pendingName.isEmpty && connectedName == pendingName
39
+ }
40
+
41
+ static func shouldRecover(
42
+ isConnected: Bool,
43
+ connectedName: String,
44
+ connectedIdentifier: String,
45
+ pendingName: String,
46
+ pendingIdentifier: String
47
+ ) -> Bool {
48
+ isConnected && matches(
49
+ connectedName: connectedName,
50
+ connectedIdentifier: connectedIdentifier,
51
+ pendingName: pendingName,
52
+ pendingIdentifier: pendingIdentifier
53
+ )
54
+ }
55
+ }
56
+
28
57
  // MARK: - BlePhotoUploadService
29
58
 
30
59
  class BlePhotoUploadService {
@@ -857,20 +886,66 @@ extension MentraLive: CBCentralManagerDelegate {
857
886
  }
858
887
  }
859
888
 
860
- func handleDiscoveredPeripheral(_ peripheral: CBPeripheral, rssi: NSNumber? = nil) {
889
+ func handleDiscoveredPeripheral(
890
+ _ peripheral: CBPeripheral,
891
+ advertisementData: [String: Any] = [:],
892
+ rssi: NSNumber? = nil
893
+ ) {
861
894
  guard let name = peripheral.name else { return }
862
895
 
863
896
  // Check for compatible device names
864
897
  if name == "Xy_A" || name.hasPrefix("XyBLE_") || name.hasPrefix("MENTRA_LIVE_BLE")
865
898
  || name.hasPrefix("MENTRA_LIVE_BT") || name.lowercased().hasPrefix("mentra_live")
866
899
  {
900
+ let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME)
901
+ let isReconnectTarget = savedDeviceName != nil && savedDeviceName == name
902
+ if !isReconnectTarget && advertisesPairingFlag(advertisementData)
903
+ && !isPairingDiscoverable(advertisementData)
904
+ {
905
+ // Keep nearby secure units visible with pairing-mode guidance, while RN blocks
906
+ // the connection until a pairable advertisement arrives.
907
+ Bridge.log(
908
+ "LIVE: Nearby \(name) is secure firmware not in pairing mode — exposing as non-pairable"
909
+ )
910
+ discoveredPeripherals[name] = peripheral
911
+ cacheAdvPairing(
912
+ name,
913
+ pairingMode: false,
914
+ pairingCode: nil,
915
+ securePairingCapable: true
916
+ )
917
+ emitDiscoveredDeviceFromCache(
918
+ name,
919
+ identifier: peripheral.identifier.uuidString,
920
+ rssi: rssi?.intValue
921
+ )
922
+ return
923
+ }
924
+
867
925
  let glassType = name == "Xy_A" ? "Standard" : "K900"
868
- Bridge.log("Found compatible \(glassType) glasses device: \(name)")
926
+ let trailer = parseSecurePairingTrailer(advertisementData)
927
+ Bridge.log(
928
+ trailer.secureCapable
929
+ ? "Found compatible \(glassType) glasses device: \(name) (pairingMode=\(trailer.pairingMode))"
930
+ : "Found compatible \(glassType) glasses device: \(name) (legacy firmware, pairable)"
931
+ )
869
932
 
870
933
  // Store the peripheral
871
934
  discoveredPeripherals[name] = peripheral
872
935
 
873
- emitDiscoveredDevice(name, identifier: peripheral.identifier.uuidString, rssi: rssi?.intValue)
936
+ cacheAdvPairing(
937
+ name,
938
+ // Field firmware has no pairing flag. Treat it as pairable so the
939
+ // new Mentra App can still find old glasses.
940
+ pairingMode: trailer.secureCapable ? trailer.pairingMode : true,
941
+ pairingCode: trailer.pairingCode,
942
+ securePairingCapable: trailer.secureCapable
943
+ )
944
+ emitDiscoveredDeviceFromCache(
945
+ name,
946
+ identifier: peripheral.identifier.uuidString,
947
+ rssi: rssi?.intValue
948
+ )
874
949
 
875
950
  // Check if this is the device we want to connect to
876
951
  if let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME),
@@ -887,21 +962,42 @@ extension MentraLive: CBCentralManagerDelegate {
887
962
 
888
963
  nonisolated func centralManager(
889
964
  _: CBCentralManager, didDiscover peripheral: CBPeripheral,
890
- advertisementData _: [String: Any], rssi: NSNumber
965
+ advertisementData: [String: Any], rssi: NSNumber
891
966
  ) {
892
967
  DispatchQueue.main.async { [weak self] in
893
- self?.handleDiscoveredPeripheral(peripheral, rssi: rssi)
968
+ self?.handleDiscoveredPeripheral(peripheral, advertisementData: advertisementData, rssi: rssi)
894
969
  }
895
970
  }
896
971
 
897
972
  nonisolated func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
898
973
  DispatchQueue.main.async { [weak self] in
899
974
  guard let self else { return }
975
+ let matchesActiveAttempt = self.connectingPeripheral === peripheral
976
+ guard MentraLiveConnectionAttemptPolicy.shouldAcceptDidConnect(
977
+ pairingYieldActive: self.pairingYieldActive,
978
+ matchesActiveAttempt: matchesActiveAttempt
979
+ ) else {
980
+ Bridge.log(
981
+ "LIVE: Rejecting stale connection callback during pairing yield or after attempt replacement: \(peripheral.identifier)"
982
+ )
983
+ self.stopConnectionTimeout()
984
+ if self.connectingPeripheral === peripheral {
985
+ self.connectingPeripheral = nil
986
+ }
987
+ if self.connectedPeripheral === peripheral {
988
+ self.connectedPeripheral = nil
989
+ }
990
+ self.isConnecting = false
991
+ self.centralManager?.cancelPeripheralConnection(peripheral)
992
+ return
993
+ }
900
994
  Bridge.log("Connected to GATT server, discovering services...")
901
995
 
902
996
  self.stopConnectionTimeout()
903
997
  self.isConnecting = false
998
+ self.connectingPeripheral = nil
904
999
  self.connectedPeripheral = peripheral
1000
+ self.emitConnectedPendingDeviceForPairingScan()
905
1001
 
906
1002
  // Save device name and address for future reconnection
907
1003
  if let name = peripheral.name {
@@ -939,10 +1035,17 @@ extension MentraLive: CBCentralManagerDelegate {
939
1035
  }
940
1036
 
941
1037
  nonisolated func centralManager(
942
- _: CBCentralManager, didDisconnectPeripheral _: CBPeripheral, error _: Error?
1038
+ _: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?
943
1039
  ) {
944
1040
  DispatchQueue.main.async { [weak self] in
945
1041
  guard let self else { return }
1042
+ let isCurrent =
1043
+ self.connectedPeripheral === peripheral || self.connectingPeripheral === peripheral
1044
+ if !isCurrent {
1045
+ Bridge.log("LIVE: Ignoring stale disconnect for \(peripheral.identifier)")
1046
+ return
1047
+ }
1048
+ self.connectingPeripheral = nil
946
1049
  Bridge.log("LIVE: Disconnected from GATT server")
947
1050
 
948
1051
  self.isConnecting = false
@@ -962,6 +1065,11 @@ extension MentraLive: CBCentralManagerDelegate {
962
1065
  self.txCharacteristic = nil
963
1066
  self.rxCharacteristic = nil
964
1067
 
1068
+ if self.pairingYieldAwaitingReclaim, Self.isPairingAuthFailure(error) {
1069
+ self.clearSavedGlassesAfterOwnerLoss(reason: "ios_auth_fail")
1070
+ return
1071
+ }
1072
+
965
1073
  // Attempt reconnection if not killed
966
1074
  if !self.isKilled {
967
1075
  self.handleReconnection()
@@ -969,10 +1077,17 @@ extension MentraLive: CBCentralManagerDelegate {
969
1077
  }
970
1078
  }
971
1079
 
972
- nonisolated func centralManager(_: CBCentralManager, didFailToConnect _: CBPeripheral, error: Error?) {
1080
+ nonisolated func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
973
1081
  let errorDescription = error?.localizedDescription ?? "Unknown error"
974
1082
  DispatchQueue.main.async { [weak self] in
975
1083
  guard let self else { return }
1084
+ let isCurrent =
1085
+ self.connectedPeripheral === peripheral || self.connectingPeripheral === peripheral
1086
+ if !isCurrent {
1087
+ Bridge.log("LIVE: Ignoring stale connect failure for \(peripheral.identifier)")
1088
+ return
1089
+ }
1090
+ self.connectingPeripheral = nil
976
1091
  Bridge.log("LIVE: Failed to connect to peripheral: \(errorDescription)")
977
1092
 
978
1093
  self.stopConnectionTimeout()
@@ -981,6 +1096,11 @@ extension MentraLive: CBCentralManagerDelegate {
981
1096
  self.connectedPeripheral = nil
982
1097
  self.updateConnectionState(ConnTypes.DISCONNECTED)
983
1098
 
1099
+ if self.pairingYieldAwaitingReclaim, Self.isPairingAuthFailure(error) {
1100
+ self.clearSavedGlassesAfterOwnerLoss(reason: "ios_auth_fail")
1101
+ return
1102
+ }
1103
+
984
1104
  if !self.isKilled {
985
1105
  self.handleReconnection()
986
1106
  }
@@ -1276,6 +1396,39 @@ class MentraLive: NSObject, SGCManager {
1276
1396
  private let BLOCK_AUDIO_DUPLEX = false
1277
1397
  private static let voiceActivityDetectionSwitchType = 8
1278
1398
  private static let loudnessGateSwitchType = 10
1399
+ private func pairingAdvertisement(
1400
+ _ advertisementData: [String: Any]
1401
+ ) -> MentraLivePairingAdvertisement? {
1402
+ MentraLivePairingAdvertisement.parse(
1403
+ coreBluetoothManufacturerData:
1404
+ advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
1405
+ )
1406
+ }
1407
+
1408
+ private func isPairingDiscoverable(_ advertisementData: [String: Any]) -> Bool {
1409
+ pairingAdvertisement(advertisementData)?.pairingMode == true
1410
+ }
1411
+
1412
+ private struct SecurePairingTrailer {
1413
+ let pairingMode: Bool
1414
+ let pairingCode: String?
1415
+ let secureCapable: Bool
1416
+ }
1417
+
1418
+ private func parseSecurePairingTrailer(_ advertisementData: [String: Any]) -> SecurePairingTrailer {
1419
+ guard let advertisement = pairingAdvertisement(advertisementData) else {
1420
+ return SecurePairingTrailer(pairingMode: false, pairingCode: nil, secureCapable: false)
1421
+ }
1422
+ return SecurePairingTrailer(
1423
+ pairingMode: advertisement.pairingMode,
1424
+ pairingCode: advertisement.pairingCode,
1425
+ secureCapable: true
1426
+ )
1427
+ }
1428
+
1429
+ private func advertisesPairingFlag(_ advertisementData: [String: Any]) -> Bool {
1430
+ pairingAdvertisement(advertisementData) != nil
1431
+ }
1279
1432
 
1280
1433
  var connectionState: String = ConnTypes.DISCONNECTED
1281
1434
 
@@ -1290,6 +1443,8 @@ class MentraLive: NSObject, SGCManager {
1290
1443
  // return, unlike Android).
1291
1444
  DeviceStore.shared.apply("glasses", "serialNumber", "")
1292
1445
  DeviceStore.shared.apply("glasses", "bluetoothMacAddress", "")
1446
+ DeviceStore.shared.apply("glasses", "wifiMacAddress", "")
1447
+ DeviceStore.shared.apply("glasses", "hotspotOtaVersion", 0)
1293
1448
  }
1294
1449
  connectionState = state
1295
1450
  DeviceStore.shared.apply("glasses", "connectionState", state)
@@ -1343,14 +1498,26 @@ class MentraLive: NSObject, SGCManager {
1343
1498
  func forget() {
1344
1499
  Bridge.log("LIVE: Forgetting Mentra Live glasses")
1345
1500
 
1501
+ // Clear saved device name so the pairing filter is not bypassed on rescan
1502
+ UserDefaults.standard.removeObject(forKey: PREFS_DEVICE_NAME)
1503
+
1346
1504
  // Stop scanning first
1347
1505
  if isScanning {
1348
1506
  stopScan()
1349
1507
  emitStopScanEvent()
1350
1508
  }
1351
1509
 
1352
- // Then do full cleanup (disconnect + clear all references)
1353
- destroy()
1510
+ // Then do full cleanup after the unpair write can leave GATT.
1511
+ sendJson(["type": "unpair"], wakeUp: true, requireAck: false)
1512
+ unpairFlushWorkItem?.cancel()
1513
+ unpairFlushPending = true
1514
+ let work = DispatchWorkItem { [self] in
1515
+ self.unpairFlushPending = false
1516
+ self.unpairFlushWorkItem = nil
1517
+ self.destroy()
1518
+ }
1519
+ unpairFlushWorkItem = work
1520
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: work)
1354
1521
  }
1355
1522
 
1356
1523
  var type = "Mentra Live"
@@ -1428,7 +1595,6 @@ class MentraLive: NSObject, SGCManager {
1428
1595
  private var lc3ReadCharacteristic: CBCharacteristic?
1429
1596
  private var lc3WriteCharacteristic: CBCharacteristic?
1430
1597
  private var supportsLC3Audio = true
1431
- private var lastReceivedLc3Sequence: Int8 = -1
1432
1598
  private let LC3_FRAME_SIZE = 40 // bytes per LC3 frame
1433
1599
  private let MICBEAT_INTERVAL_MS: TimeInterval = 30 * 60 // 30 minutes in seconds
1434
1600
  private var micBeatTimer: Timer?
@@ -1474,6 +1640,8 @@ class MentraLive: NSObject, SGCManager {
1474
1640
  private var centralManager: CBCentralManager?
1475
1641
 
1476
1642
  private var connectedPeripheral: CBPeripheral?
1643
+ /// Peripheral for the in-flight connect; used to ignore stale disconnect/fail callbacks.
1644
+ private var connectingPeripheral: CBPeripheral?
1477
1645
  private var txCharacteristic: CBCharacteristic?
1478
1646
  private var rxCharacteristic: CBCharacteristic?
1479
1647
  private let bes2700MtuLimit = 509
@@ -1481,8 +1649,16 @@ class MentraLive: NSObject, SGCManager {
1481
1649
 
1482
1650
  // State Tracking
1483
1651
  private var isScanning = false
1652
+ private var manualDiscoveryActive = false
1484
1653
  private var isConnecting = false
1485
1654
  private var isKilled = false
1655
+ /// Glasses opened pairing window — stand down without forgetting identity/bonds.
1656
+ private var pairingYieldActive = false
1657
+ private var pairingYieldAwaitingReclaim = false
1658
+ private var pairingYieldEndWorkItem: DispatchWorkItem?
1659
+ /// Hold GATT teardown until the unpair write can leave the phone.
1660
+ private var unpairFlushPending = false
1661
+ private var unpairFlushWorkItem: DispatchWorkItem?
1486
1662
  private var reconnectAttempts = 0
1487
1663
  private var isNewVersion = false
1488
1664
  private var peerWireProtocolVersion = 0
@@ -1601,6 +1777,16 @@ class MentraLive: NSObject, SGCManager {
1601
1777
  // MARK: - React Native Interface
1602
1778
 
1603
1779
  private var discoveredPeripherals = [String: CBPeripheral]() // name -> peripheral
1780
+ /// Last advertisement pairing trailer for a discovered name. Re-emits (already-
1781
+ /// connected / scan replay) must not drop these flags: omitting them makes a
1782
+ /// secure idle unit look like pairable legacy firmware.
1783
+ private var discoveredAdvPairing = [String: CachedAdvPairing]()
1784
+
1785
+ private struct CachedAdvPairing {
1786
+ let pairingMode: Bool
1787
+ let pairingCode: String?
1788
+ let securePairingCapable: Bool
1789
+ }
1604
1790
 
1605
1791
  func findCompatibleDevices() {
1606
1792
  Bridge.log("Finding compatible Mentra Live glasses")
@@ -1618,11 +1804,95 @@ class MentraLive: NSObject, SGCManager {
1618
1804
  // clear the saved device name:
1619
1805
  UserDefaults.standard.set("", forKey: PREFS_DEVICE_NAME)
1620
1806
 
1807
+ manualDiscoveryActive = true
1621
1808
  startScan()
1809
+ emitConnectedPendingDeviceForPairingScan()
1622
1810
  }
1623
1811
  }
1624
1812
 
1813
+ func isPairingYieldActive() -> Bool {
1814
+ pairingYieldActive
1815
+ }
1816
+
1817
+ private func enterPairingYield(windowMs: Int) {
1818
+ pairingYieldActive = true
1819
+ pairingYieldAwaitingReclaim = false
1820
+ pairingYieldEndWorkItem?.cancel()
1821
+ if isScanning {
1822
+ stopScan()
1823
+ }
1824
+ // Publish disconnect immediately for Phone A UI. didDisconnectPeripheral may lag
1825
+ // or race with cancelPeripheralConnection; do not wait on it for stand-down.
1826
+ isConnecting = false
1827
+ connected = false
1828
+ fullyBooted = false
1829
+ glassesSessionId = nil
1830
+ readinessCompletedThisBleSession = false
1831
+ rgbLedAuthorityClaimed = false
1832
+ stopAllTimers()
1833
+ closeL2capFileChannel()
1834
+ txCharacteristic = nil
1835
+ rxCharacteristic = nil
1836
+ updateConnectionState(ConnTypes.DISCONNECTED)
1837
+ if let peripheral = connectedPeripheral {
1838
+ centralManager?.cancelPeripheralConnection(peripheral)
1839
+ }
1840
+ connectedPeripheral = nil
1841
+ connectingPeripheral = nil
1842
+ // Stand down for the whole window. Probing GATT during yield races the new
1843
+ // phone for the single BLE slot and is exactly what entering_pairing_mode forbids.
1844
+
1845
+ let work = DispatchWorkItem { [weak self] in
1846
+ guard let self else { return }
1847
+ Bridge.log("LIVE: Pairing yield ended — resume reconnect if still owned")
1848
+ self.pairingYieldActive = false
1849
+ self.pairingYieldAwaitingReclaim = true
1850
+ self.pairingYieldEndWorkItem = nil
1851
+ if !self.isKilled, self.connectedPeripheral == nil {
1852
+ self.handleReconnection()
1853
+ }
1854
+ }
1855
+ pairingYieldEndWorkItem = work
1856
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(windowMs), execute: work)
1857
+ }
1858
+
1859
+ /// Pairing/auth failures after yield — CoreBluetooth codes, not localized strings.
1860
+ /// Connection timeouts are not owner-loss.
1861
+ private static func isPairingAuthFailure(_ error: Error?) -> Bool {
1862
+ guard let error = error as NSError? else { return false }
1863
+ if error.domain == CBATTErrorDomain {
1864
+ return error.code == CBATTError.insufficientAuthentication.rawValue
1865
+ || error.code == CBATTError.insufficientEncryption.rawValue
1866
+ }
1867
+ if error.domain == CBErrorDomain {
1868
+ return error.code == CBError.peerRemovedPairingInformation.rawValue
1869
+ }
1870
+ return false
1871
+ }
1872
+
1873
+ private func clearSavedGlassesAfterOwnerLoss(reason: String) {
1874
+ Bridge.log("LIVE: Owner loss (\(reason)) — clearing saved glasses")
1875
+ pairingYieldAwaitingReclaim = false
1876
+ pairingYieldActive = false
1877
+ pairingYieldEndWorkItem?.cancel()
1878
+ pairingYieldEndWorkItem = nil
1879
+ forget()
1880
+ Task { @MainActor in
1881
+ DeviceStore.shared.apply("bluetooth", "default_wearable", "")
1882
+ DeviceStore.shared.apply("bluetooth", "device_name", "")
1883
+ DeviceStore.shared.apply("bluetooth", "device_address", "")
1884
+ }
1885
+ Bridge.saveSetting("default_wearable", "")
1886
+ Bridge.saveSetting("device_name", "")
1887
+ Bridge.saveSetting("device_address", "")
1888
+ Bridge.sendTypedMessage("owner_replaced", body: ["reason": reason])
1889
+ }
1890
+
1625
1891
  func connectById(_ deviceName: String) {
1892
+ if pairingYieldActive {
1893
+ Bridge.log("LIVE: connectById blocked — pairing yield active")
1894
+ return
1895
+ }
1626
1896
  Bridge.log("connectById: \(deviceName)")
1627
1897
  // Save the device name for future reconnection
1628
1898
  UserDefaults.standard.set(deviceName, forKey: PREFS_DEVICE_NAME)
@@ -1647,7 +1917,7 @@ class MentraLive: NSObject, SGCManager {
1647
1917
  {
1648
1918
  Bridge.log("Found already-connected peripheral: \(name)")
1649
1919
  discoveredPeripherals[name] = peripheral
1650
- emitDiscoveredDevice(name)
1920
+ emitDiscoveredDeviceFromCache(name)
1651
1921
 
1652
1922
  // Check if this is the device we want
1653
1923
  if let savedDeviceName = UserDefaults.standard.string(
@@ -1673,6 +1943,10 @@ class MentraLive: NSObject, SGCManager {
1673
1943
  }
1674
1944
 
1675
1945
  @objc func disconnect() {
1946
+ if unpairFlushPending {
1947
+ Bridge.log("LIVE: disconnect deferred — waiting for unpair write")
1948
+ return
1949
+ }
1676
1950
  Bridge.log("LIVE: disconnect() -Disconnecting from Mentra Live glasses")
1677
1951
 
1678
1952
  // if rgbLedAuthorityClaimed {
@@ -1698,6 +1972,8 @@ class MentraLive: NSObject, SGCManager {
1698
1972
 
1699
1973
  /// Start the micbeat mechanism to keep LC3 audio streaming active
1700
1974
  private func startMicBeat() {
1975
+ // A resumed custom-audio stream starts a new sequence domain.
1976
+ DeviceManager.shared.resetMicSequenceBaseline()
1701
1977
  Bridge.log("LIVE: 🎤 Starting micbeat mechanism")
1702
1978
  micBeatCount = 0
1703
1979
 
@@ -2094,7 +2370,10 @@ class MentraLive: NSObject, SGCManager {
2094
2370
  // MARK: - BLE Scanning
2095
2371
 
2096
2372
  private func startScan() {
2097
- // guard !isScanning else { return }
2373
+ if pairingYieldActive {
2374
+ Bridge.log("LIVE: startScan blocked — pairing yield active")
2375
+ return
2376
+ }
2098
2377
 
2099
2378
  guard centralManager!.state == .poweredOn else {
2100
2379
  Bridge.log("Attempting to scan but bluetooth is not powered on.")
@@ -2102,6 +2381,7 @@ class MentraLive: NSObject, SGCManager {
2102
2381
  }
2103
2382
 
2104
2383
  Bridge.log("Starting BLE scan for Mentra Live glasses")
2384
+ discoveredAdvPairing.removeAll()
2105
2385
  isScanning = true
2106
2386
 
2107
2387
  startReadinessCheckLoop()
@@ -2118,12 +2398,6 @@ class MentraLive: NSObject, SGCManager {
2118
2398
 
2119
2399
  centralManager?.scanForPeripherals(withServices: nil, options: scanOptions)
2120
2400
 
2121
- // emit already discovered peripherals:
2122
- for (_, peripheral) in discoveredPeripherals {
2123
- Bridge.log("LIVE: (Already discovered) peripheral: \(peripheral.name ?? "Unknown")")
2124
- emitDiscoveredDevice(peripheral.name!)
2125
- }
2126
-
2127
2401
  // var dName = DeviceManager.shared.deviceName
2128
2402
  // if dName.isEmpty {
2129
2403
  // dName = "MENTRA_LIVE"
@@ -2141,6 +2415,7 @@ class MentraLive: NSObject, SGCManager {
2141
2415
  }
2142
2416
 
2143
2417
  func stopScan() {
2418
+ manualDiscoveryActive = false
2144
2419
  guard isScanning else { return }
2145
2420
 
2146
2421
  centralManager?.stopScan()
@@ -2152,10 +2427,16 @@ class MentraLive: NSObject, SGCManager {
2152
2427
  // MARK: - Connection Management
2153
2428
 
2154
2429
  private func connectToDevice(_ peripheral: CBPeripheral) {
2430
+ if pairingYieldActive {
2431
+ Bridge.log("LIVE: connectToDevice blocked — pairing yield active")
2432
+ isConnecting = false
2433
+ return
2434
+ }
2155
2435
  Bridge.log("LIVE: Connecting to device: \(peripheral.identifier.uuidString)")
2156
2436
 
2157
2437
  isConnecting = true
2158
2438
  updateConnectionState(ConnTypes.CONNECTING)
2439
+ connectingPeripheral = peripheral
2159
2440
  connectedPeripheral = peripheral
2160
2441
  peripheral.delegate = self
2161
2442
 
@@ -2200,6 +2481,11 @@ class MentraLive: NSObject, SGCManager {
2200
2481
  return
2201
2482
  }
2202
2483
 
2484
+ if pairingYieldActive {
2485
+ Bridge.log("LIVE: Reconnection aborted - pairing yield active")
2486
+ return
2487
+ }
2488
+
2203
2489
  // Check if we've exceeded max attempts
2204
2490
  if reconnectAttempts >= MAX_RECONNECT_ATTEMPTS {
2205
2491
  Bridge.log("LIVE: Maximum reconnection attempts reached (\(MAX_RECONNECT_ATTEMPTS))")
@@ -2232,6 +2518,10 @@ class MentraLive: NSObject, SGCManager {
2232
2518
  guard let self = self else { return }
2233
2519
 
2234
2520
  // Use peripheral presence, not connectionState: we stay CONNECTING during backoff until scan/connect.
2521
+ if self.pairingYieldActive {
2522
+ Bridge.log("LIVE: Reconnection aborted - pairing yield active")
2523
+ return
2524
+ }
2235
2525
  if self.connectedPeripheral == nil, !self.isKilled {
2236
2526
  // Check for last known device name to start scan
2237
2527
  if let lastDeviceName = UserDefaults.standard.string(
@@ -2461,11 +2751,21 @@ class MentraLive: NSObject, SGCManager {
2461
2751
 
2462
2752
  switch type {
2463
2753
  case "glasses_ready":
2754
+ // A queued glasses_ready from the connection we just cancelled for yield
2755
+ // must not close the pairing window. Only accept readiness on a live GATT.
2756
+ if pairingYieldActive && connectedPeripheral == nil {
2757
+ Bridge.log("LIVE: Ignoring stale glasses_ready during pairing yield")
2758
+ return
2759
+ }
2464
2760
  // glasses_ready is a REMOTE wire-session reset: the glasses ran
2465
2761
  // onTransportReset() before sending it, so their side is back on legacy
2466
2762
  // framing regardless of what this side negotiated earlier. Start a fresh
2467
2763
  // wire epoch (clears v2-active state that would otherwise gate the
2468
2764
  // handshake off), then negotiate from the caps this message advertises.
2765
+ pairingYieldAwaitingReclaim = false
2766
+ pairingYieldActive = false
2767
+ pairingYieldEndWorkItem?.cancel()
2768
+ pairingYieldEndWorkItem = nil
2469
2769
  resetWireNegotiationState()
2470
2770
  parsePeerWireCaps(json)
2471
2771
  maybeSendWireHandshake()
@@ -2617,6 +2917,26 @@ class MentraLive: NSObject, SGCManager {
2617
2917
  case "pong":
2618
2918
  Bridge.log("LIVE: Received pong response - connection healthy")
2619
2919
 
2920
+ case "entering_pairing_mode":
2921
+ let windowMs = max(5_000, min(180_000, json["window_ms"] as? Int ?? 120_000))
2922
+ Bridge.log("LIVE: Glasses entering pairing mode — yield \(windowMs)ms (no forget)")
2923
+ enterPairingYield(windowMs: windowMs)
2924
+ let body: [String: Any] = [
2925
+ "window_ms": windowMs,
2926
+ "reason": json["reason"] as? String ?? "user_gesture",
2927
+ ]
2928
+ Bridge.sendTypedMessage("entering_pairing_mode", body: body)
2929
+
2930
+ case "pairing_info":
2931
+ Bridge.sendPairingInfo(
2932
+ hadPreviousBond: json["had_previous_bond"] as? Bool ?? false,
2933
+ pairingCode: json["pairing_code"] as? String,
2934
+ classicBondReady: json["classic_bond_ready"] as? Bool ?? false,
2935
+ // Legacy firmware that omits this field is not secure-capable.
2936
+ securePairingCapable: json["secure_pairing_capable"] as? Bool ?? false,
2937
+ protocolVersion: json["protocol_version"] as? Int ?? 1
2938
+ )
2939
+
2620
2940
  case "imu_response", "imu_stream_response", "imu_gesture_response",
2621
2941
  "imu_gesture_subscribed", "imu_ack", "imu_error":
2622
2942
  // Handle IMU-related responses
@@ -2778,9 +3098,19 @@ class MentraLive: NSObject, SGCManager {
2778
3098
  if let systemTimeMs = fields["system_time_ms"] as? NSNumber {
2779
3099
  DeviceStore.shared.apply("glasses", "systemTimeMs", systemTimeMs.int64Value)
2780
3100
  }
3101
+ if let hotspotOtaVersion = fields["hotspot_ota_version"] as? NSNumber {
3102
+ DeviceStore.shared.apply(
3103
+ "glasses",
3104
+ "hotspotOtaVersion",
3105
+ hotspotOtaVersion.intValue
3106
+ )
3107
+ }
2781
3108
  if let bluetoothMacAddress = nonEmptyStringValue(fields, "bt_mac_address") {
2782
3109
  DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
2783
3110
  }
3111
+ if let wifiMacAddress = nonEmptyStringValue(fields, "wifi_mac_address") {
3112
+ DeviceStore.shared.apply("glasses", "wifiMacAddress", wifiMacAddress)
3113
+ }
2784
3114
  if let serialNumber = nonEmptyStringValue(fields, "serial_number") {
2785
3115
  DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
2786
3116
  }
@@ -3219,6 +3549,17 @@ class MentraLive: NSObject, SGCManager {
3219
3549
  sendJson(json, wakeUp: true)
3220
3550
  }
3221
3551
 
3552
+ func sendWifiAdbState(_ enabled: Bool) {
3553
+ Bridge.log("LIVE: 🔧 Sending Wi-Fi ADB state: \(enabled)")
3554
+
3555
+ let json: [String: Any] = [
3556
+ "type": "set_wifi_adb_state",
3557
+ "enabled": enabled,
3558
+ ]
3559
+
3560
+ sendJson(json, wakeUp: true)
3561
+ }
3562
+
3222
3563
  func sendSetSystemTime(_ timestampMs: Int64) {
3223
3564
  Bridge.log("LIVE: ⏰ Sending set_system_time: \(timestampMs)")
3224
3565
 
@@ -3338,7 +3679,6 @@ class MentraLive: NSObject, SGCManager {
3338
3679
  {
3339
3680
  json["ota_version_url"] = otaVersionUrl
3340
3681
  }
3341
-
3342
3682
  sendJson(json, wakeUp: true)
3343
3683
  }
3344
3684
 
@@ -3509,17 +3849,9 @@ class MentraLive: NSObject, SGCManager {
3509
3849
  return
3510
3850
  }
3511
3851
 
3512
- let sequenceNumber = Int8(bitPattern: data[1])
3852
+ let sequenceNumber = Int(data[1])
3513
3853
  let lc3Data = data.subdata(in: 2 ..< data.count)
3514
3854
 
3515
- // Validate sequence number for packet loss detection
3516
- if lastReceivedLc3Sequence != -1 && (lastReceivedLc3Sequence &+ 1) != sequenceNumber {
3517
- Bridge.log(
3518
- "LIVE: LC3 packet sequence mismatch. Expected: \(lastReceivedLc3Sequence &+ 1), Got: \(sequenceNumber)"
3519
- )
3520
- }
3521
- lastReceivedLc3Sequence = sequenceNumber
3522
-
3523
3855
  // // Decode LC3 to PCM using existing PcmConverter
3524
3856
  // let pcmConverter = PcmConverter()
3525
3857
  // guard let pcmData = pcmConverter.decode(lc3Data) as? Data, pcmData.count > 0 else {
@@ -3533,7 +3865,7 @@ class MentraLive: NSObject, SGCManager {
3533
3865
  // Bridge.log(
3534
3866
  // "LIVE: Processed LC3 audio seq=\(sequenceNumber), \(lc3Data.count) bytes"
3535
3867
  // )
3536
- DeviceManager.shared.handleGlassesMicData(lc3Data, 40)
3868
+ DeviceManager.shared.handleGlassesMicData(lc3Data, 40, sequenceNumber: sequenceNumber)
3537
3869
 
3538
3870
  // Bridge.log(
3539
3871
  // "LIVE: Processed LC3 audio seq=\(sequenceNumber), \(lc3Data.count)→\(pcmData.count) bytes"
@@ -5201,8 +5533,82 @@ class MentraLive: NSObject, SGCManager {
5201
5533
 
5202
5534
  // MARK: - Event Emission
5203
5535
 
5204
- private func emitDiscoveredDevice(_ name: String, identifier: String = "", rssi: Int? = nil) {
5205
- Bridge.sendDiscoveredDevice("Mentra Live", name, deviceAddress: identifier, rssi: rssi)
5536
+ /// A selected pairing target can stop advertising once GATT connects, before readiness
5537
+ /// promotes it to the default device. Re-emit only that explicit pending target; an
5538
+ /// established owner's connected glasses have no pending identity and remain hidden.
5539
+ private func emitConnectedPendingDeviceForPairingScan() {
5540
+ guard manualDiscoveryActive, !pairingYieldActive, let peripheral = connectedPeripheral,
5541
+ let name = peripheral.name,
5542
+ MentraLivePendingPairingTarget.shouldRecover(
5543
+ isConnected: peripheral.state == .connected,
5544
+ connectedName: name,
5545
+ connectedIdentifier: peripheral.identifier.uuidString,
5546
+ pendingName: DeviceStore.shared.get("bluetooth", "pending_device_name") as? String ?? "",
5547
+ pendingIdentifier: DeviceStore.shared.get("bluetooth", "pending_device_address") as? String ?? ""
5548
+ )
5549
+ else {
5550
+ return
5551
+ }
5552
+
5553
+ Bridge.log("LIVE: Pairing scan: recovering connected pending target \(name) (\(peripheral.identifier))")
5554
+ emitDiscoveredDevice(
5555
+ name,
5556
+ identifier: peripheral.identifier.uuidString,
5557
+ pairingMode: true,
5558
+ pairingCode: nil,
5559
+ securePairingCapable: DeviceStore.shared.get(
5560
+ "bluetooth",
5561
+ "pending_device_secure_pairing_capable"
5562
+ ) as? Bool
5563
+ )
5564
+ }
5565
+
5566
+ private func cacheAdvPairing(
5567
+ _ name: String,
5568
+ pairingMode: Bool,
5569
+ pairingCode: String?,
5570
+ securePairingCapable: Bool
5571
+ ) {
5572
+ discoveredAdvPairing[name] = CachedAdvPairing(
5573
+ pairingMode: pairingMode,
5574
+ pairingCode: pairingCode,
5575
+ securePairingCapable: securePairingCapable
5576
+ )
5577
+ }
5578
+
5579
+ private func emitDiscoveredDeviceFromCache(
5580
+ _ name: String,
5581
+ identifier: String = "",
5582
+ rssi: Int? = nil
5583
+ ) {
5584
+ let cached = discoveredAdvPairing[name]
5585
+ emitDiscoveredDevice(
5586
+ name,
5587
+ identifier: identifier,
5588
+ rssi: rssi,
5589
+ pairingMode: cached?.pairingMode,
5590
+ pairingCode: cached?.pairingCode,
5591
+ securePairingCapable: cached?.securePairingCapable
5592
+ )
5593
+ }
5594
+
5595
+ private func emitDiscoveredDevice(
5596
+ _ name: String,
5597
+ identifier: String = "",
5598
+ rssi: Int? = nil,
5599
+ pairingMode: Bool? = nil,
5600
+ pairingCode: String? = nil,
5601
+ securePairingCapable: Bool? = nil
5602
+ ) {
5603
+ Bridge.sendDiscoveredDevice(
5604
+ "Mentra Live",
5605
+ name,
5606
+ deviceAddress: identifier,
5607
+ rssi: rssi,
5608
+ pairingMode: pairingMode,
5609
+ pairingCode: pairingCode,
5610
+ securePairingCapable: securePairingCapable
5611
+ )
5206
5612
  }
5207
5613
 
5208
5614
  private func emitStopScanEvent() {
@@ -5291,6 +5697,9 @@ class MentraLive: NSObject, SGCManager {
5291
5697
  Bridge.log("Destroying MentraLiveManager")
5292
5698
 
5293
5699
  isKilled = true
5700
+ unpairFlushWorkItem?.cancel()
5701
+ unpairFlushWorkItem = nil
5702
+ unpairFlushPending = false
5294
5703
 
5295
5704
  // Stop scanning
5296
5705
  if isScanning {
@@ -5324,6 +5733,8 @@ class MentraLive: NSObject, SGCManager {
5324
5733
  DeviceStore.shared.apply("glasses", "hotspotGatewayIp", "")
5325
5734
 
5326
5735
  connectedPeripheral = nil
5736
+ discoveredPeripherals.removeAll()
5737
+ discoveredAdvPairing.removeAll()
5327
5738
  centralManager?.delegate = nil
5328
5739
  centralManager = nil
5329
5740
 
@@ -6533,6 +6944,17 @@ extension MentraLive {
6533
6944
  stopVideoRecording(requestId: requestId, webhookUrl: nil, authToken: nil)
6534
6945
  }
6535
6946
 
6947
+ func queryVideoRecordingStatus(requestId: String) {
6948
+ guard connectionState == ConnTypes.CONNECTED else {
6949
+ Bridge.log("Cannot query video recording status - not connected")
6950
+ return
6951
+ }
6952
+ sendJson([
6953
+ "type": "get_video_recording_status",
6954
+ "requestId": requestId,
6955
+ ])
6956
+ }
6957
+
6536
6958
  func stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
6537
6959
  Bridge.log(
6538
6960
  "Stopping video recording on glasses: requestId=\(requestId), webhook=\((webhookUrl?.isEmpty ?? true) ? "none" : "set")"