@mentra/bluetooth-sdk 3.1.0-dev.15 → 3.1.0-dev.17

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.
@@ -3,9 +3,9 @@ package com.mentra.bluetoothsdk
3
3
  /** Generated by release CI. Do not edit in a release checkout. */
4
4
  internal object GeneratedReleaseMetadata {
5
5
  const val FAMILY_BASE_VERSION: String = "3.1.0"
6
- const val RELEASE_IDENTITY: String = "3.1.0-dev.15"
7
- const val RELEASE_SET_ID: String = "mentra-3.1.0-dev.15"
8
- const val SOURCE_COMMIT: String = "b22afd316787f99de5df15b323b08f5e8d6c40f5"
9
- const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.15/mentra-live-ota-3.1.0-dev.15.json"
6
+ const val RELEASE_IDENTITY: String = "3.1.0-dev.17"
7
+ const val RELEASE_SET_ID: String = "mentra-3.1.0-dev.17"
8
+ const val SOURCE_COMMIT: String = "84c1e737fddc23bf9abb9b1916937c5b7e892f54"
9
+ const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.17/mentra-live-ota-3.1.0-dev.17.json"
10
10
  const val OTA_MANIFEST_SHA256: String = "47e576dec6afd4cc6ccbc758b12fe65b33b180382a899f8098dfcd11f7bcb2a6"
11
11
  }
@@ -284,6 +284,7 @@ class MentraBluetoothSdk private constructor(
284
284
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "project_name", device.projectName ?: "")
285
285
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
286
286
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
287
+ DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
287
288
  } finally {
288
289
  suppressDefaultDeviceEvents = false
289
290
  }
@@ -299,6 +300,7 @@ class MentraBluetoothSdk private constructor(
299
300
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "project_name", "")
300
301
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
301
302
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
303
+ DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
302
304
  } finally {
303
305
  suppressDefaultDeviceEvents = false
304
306
  }
@@ -428,6 +430,11 @@ class MentraBluetoothSdk private constructor(
428
430
  "pending_device_address",
429
431
  device.address ?: "",
430
432
  )
433
+ DeviceStore.apply(
434
+ ObservableStore.BLUETOOTH_CATEGORY,
435
+ "pending_device_secure_pairing_capable",
436
+ device.securePairingCapable ?: "",
437
+ )
431
438
  }
432
439
  }
433
440
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_wearable", device.model.deviceType)
@@ -454,6 +461,7 @@ class MentraBluetoothSdk private constructor(
454
461
  fun cancelConnectionAttempt() {
455
462
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
456
463
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
464
+ DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
457
465
  deviceManager.disconnect()
458
466
  }
459
467
 
@@ -464,6 +472,7 @@ class MentraBluetoothSdk private constructor(
464
472
  fun disconnect() {
465
473
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
466
474
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
475
+ DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
467
476
  deviceManager.disconnect()
468
477
  }
469
478
 
@@ -82,6 +82,33 @@ internal fun hasRequiredMentraLiveCoreCharacteristics(
82
82
  hasTxCharacteristic: Boolean,
83
83
  ): Boolean = hasRxCharacteristic && hasTxCharacteristic
84
84
 
85
+ internal fun isPendingMentraLivePairingTarget(
86
+ connectedName: String,
87
+ connectedAddress: String,
88
+ pendingName: String,
89
+ pendingAddress: String,
90
+ ): Boolean {
91
+ if (pendingAddress.isNotEmpty()) {
92
+ return connectedAddress.equals(pendingAddress, ignoreCase = true)
93
+ }
94
+ return pendingName.isNotEmpty() && connectedName == pendingName
95
+ }
96
+
97
+ internal fun shouldRecoverConnectedMentraLivePairingTarget(
98
+ isConnected: Boolean,
99
+ connectedName: String,
100
+ connectedAddress: String,
101
+ pendingName: String,
102
+ pendingAddress: String,
103
+ ): Boolean =
104
+ isConnected &&
105
+ isPendingMentraLivePairingTarget(
106
+ connectedName,
107
+ connectedAddress,
108
+ pendingName,
109
+ pendingAddress,
110
+ )
111
+
85
112
  /**
86
113
  * Smart Glasses Communicator for Mentra Live (K900) glasses Uses BLE to communicate with the
87
114
  * glasses
@@ -1265,11 +1292,6 @@ class MentraLive : SGCManager() {
1265
1292
  val generation = ++scanGeneration
1266
1293
  isScanning = true
1267
1294
  bluetoothScanner!!.startScan(filters, settings, scanCallback)
1268
- // Connected glasses stop advertising (BLE_CONNECTION_MAX=1). Pairing scan
1269
- // would otherwise return empty even though GATT is already up.
1270
- if (!isReconnecting) {
1271
- handler.post { emitConnectedDeviceForPairingScan() }
1272
- }
1273
1295
 
1274
1296
  // Set a timeout to stop scanning
1275
1297
  handler.postDelayed(
@@ -1352,29 +1374,44 @@ class MentraLive : SGCManager() {
1352
1374
  }
1353
1375
 
1354
1376
  /**
1355
- * Pairing scan listens for advertisements. A unit that is already GATT-connected
1356
- * (typical after a CTKD retry / back-out on field firmware) has stopped ADV, so
1357
- * emit it as pairable or the scan list stays empty.
1377
+ * A selected pairing target can stop advertising once its GATT link comes up, before
1378
+ * readiness promotes it to the default device. Re-emit only that explicit pending target;
1379
+ * an established owner's connected glasses have no pending name/address and stay hidden.
1358
1380
  */
1359
- private fun emitConnectedDeviceForPairingScan() {
1360
- if (!isConnected || isReconnecting || pairingYieldActive) {
1361
- return
1362
- }
1381
+ private fun emitConnectedPendingDeviceForPairingScan() {
1382
+ if (!manualDiscoveryActive || isReconnecting || pairingYieldActive) return
1363
1383
  val device = connectedDevice ?: bluetoothGatt?.device ?: return
1364
1384
  val name = safeBondedDeviceName(device)
1365
- if (!isMentraLiveBluetoothName(name)) {
1385
+ if (!isMentraLiveBluetoothName(name)) return
1386
+
1387
+ val pendingName = DeviceStore.get("bluetooth", "pending_device_name") as? String ?: ""
1388
+ val pendingAddress =
1389
+ DeviceStore.get("bluetooth", "pending_device_address") as? String ?: ""
1390
+ val pendingSecurePairingCapable =
1391
+ DeviceStore.get("bluetooth", "pending_device_secure_pairing_capable") as? Boolean
1392
+ val address = device.address ?: ""
1393
+ if (
1394
+ !shouldRecoverConnectedMentraLivePairingTarget(
1395
+ isConnected,
1396
+ name,
1397
+ address,
1398
+ pendingName,
1399
+ pendingAddress,
1400
+ )
1401
+ ) {
1366
1402
  return
1367
1403
  }
1404
+
1368
1405
  Bridge.log(
1369
- "LIVE: Pairing scan: already GATT-connected to $name — emitting as pairable (ADV off while connected)"
1406
+ "LIVE: Pairing scan: recovering connected pending target $name ($address)"
1370
1407
  )
1371
1408
  Bridge.sendDiscoveredDevice(
1372
1409
  DeviceTypes.LIVE,
1373
1410
  name,
1374
- device.address ?: "",
1411
+ address,
1375
1412
  pairingMode = true,
1376
1413
  pairingCode = null,
1377
- securePairingCapable = false,
1414
+ securePairingCapable = pendingSecurePairingCapable,
1378
1415
  )
1379
1416
  }
1380
1417
 
@@ -2075,6 +2112,7 @@ class MentraLive : SGCManager() {
2075
2112
  isConnecting = false
2076
2113
  isConnected = true
2077
2114
  connectedDevice = gatt.device
2115
+ emitConnectedPendingDeviceForPairingScan()
2078
2116
 
2079
2117
  DeviceStore.apply("glasses", "bluetoothName", connectedDevice!!.name)
2080
2118
  // Persist MAC so reconnection can use direct GATT instead of scanning
@@ -5919,6 +5957,7 @@ class MentraLive : SGCManager() {
5919
5957
 
5920
5958
  // Start scanning for BLE devices
5921
5959
  startScan()
5960
+ handler.post { emitConnectedPendingDeviceForPairingScan() }
5922
5961
  }
5923
5962
 
5924
5963
  private fun releaseStaleClassicForDiscovery() {
@@ -7,6 +7,64 @@ import org.junit.Assert.assertTrue
7
7
  import org.junit.Test
8
8
 
9
9
  class MentraLivePairingAdvertisementParserTest {
10
+ @Test
11
+ fun connectedPairingRecoveryRequiresExplicitPendingTarget() {
12
+ assertFalse(
13
+ isPendingMentraLivePairingTarget(
14
+ connectedName = "MENTRA_LIVE_BLE_OWNER",
15
+ connectedAddress = "AA:BB:CC:DD:EE:FF",
16
+ pendingName = "",
17
+ pendingAddress = "",
18
+ )
19
+ )
20
+ assertTrue(
21
+ isPendingMentraLivePairingTarget(
22
+ connectedName = "MENTRA_LIVE_BLE_TARGET",
23
+ connectedAddress = "AA:BB:CC:DD:EE:FF",
24
+ pendingName = "MENTRA_LIVE_BLE_TARGET",
25
+ pendingAddress = "",
26
+ )
27
+ )
28
+ assertTrue(
29
+ isPendingMentraLivePairingTarget(
30
+ connectedName = "MENTRA_LIVE_BLE_TARGET",
31
+ connectedAddress = "AA:BB:CC:DD:EE:FF",
32
+ pendingName = "OTHER",
33
+ pendingAddress = "aa:bb:cc:dd:ee:ff",
34
+ )
35
+ )
36
+ assertFalse(
37
+ isPendingMentraLivePairingTarget(
38
+ connectedName = "MENTRA_LIVE_BLE_TARGET",
39
+ connectedAddress = "AA:BB:CC:DD:EE:FF",
40
+ pendingName = "MENTRA_LIVE_BLE_TARGET",
41
+ pendingAddress = "11:22:33:44:55:66",
42
+ )
43
+ )
44
+ }
45
+
46
+ @Test
47
+ fun connectedPairingRecoveryRequiresActiveGattConnection() {
48
+ assertFalse(
49
+ shouldRecoverConnectedMentraLivePairingTarget(
50
+ isConnected = false,
51
+ connectedName = "MENTRA_LIVE_BLE_TARGET",
52
+ connectedAddress = "AA:BB:CC:DD:EE:FF",
53
+ pendingName = "MENTRA_LIVE_BLE_TARGET",
54
+ pendingAddress = "AA:BB:CC:DD:EE:FF",
55
+ )
56
+ )
57
+ assertTrue(
58
+ shouldRecoverConnectedMentraLivePairingTarget(
59
+ isConnected = true,
60
+ connectedName = "MENTRA_LIVE_BLE_TARGET",
61
+ connectedAddress = "AA:BB:CC:DD:EE:FF",
62
+ pendingName = "MENTRA_LIVE_BLE_TARGET",
63
+ pendingAddress = "AA:BB:CC:DD:EE:FF",
64
+ )
65
+ )
66
+ }
67
+
10
68
  @Test
11
69
  fun parsesMarkedSecurePairingAdvertisement() {
12
70
  val result = MentraLivePairingAdvertisementParser.parse(securePayload(pairingFlag = 1))
@@ -1,10 +1,10 @@
1
1
  export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
2
2
  "schemaVersion": 1,
3
3
  "familyBaseVersion": "3.1.0",
4
- "releaseIdentity": "3.1.0-dev.15",
5
- "releaseSetId": "mentra-3.1.0-dev.15",
6
- "sourceCommit": "b22afd316787f99de5df15b323b08f5e8d6c40f5",
7
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.15/mentra-live-ota-3.1.0-dev.15.json",
4
+ "releaseIdentity": "3.1.0-dev.17",
5
+ "releaseSetId": "mentra-3.1.0-dev.17",
6
+ "sourceCommit": "84c1e737fddc23bf9abb9b1916937c5b7e892f54",
7
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.17/mentra-live-ota-3.1.0-dev.17.json",
8
8
  "otaManifestSha256": "47e576dec6afd4cc6ccbc758b12fe65b33b180382a899f8098dfcd11f7bcb2a6"
9
9
  });
10
10
  //# sourceMappingURL=releaseMetadata.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,cAAc;IACjC,cAAc,EAAE,qBAAqB;IACrC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,uHAAuH;IACzI,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.1.0\",\n \"releaseIdentity\": \"3.1.0-dev.15\",\n \"releaseSetId\": \"mentra-3.1.0-dev.15\",\n \"sourceCommit\": \"b22afd316787f99de5df15b323b08f5e8d6c40f5\",\n \"otaManifestUrl\": \"https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.15/mentra-live-ota-3.1.0-dev.15.json\",\n \"otaManifestSha256\": \"47e576dec6afd4cc6ccbc758b12fe65b33b180382a899f8098dfcd11f7bcb2a6\"\n})\n"]}
1
+ {"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,cAAc;IACjC,cAAc,EAAE,qBAAqB;IACrC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,uHAAuH;IACzI,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.1.0\",\n \"releaseIdentity\": \"3.1.0-dev.17\",\n \"releaseSetId\": \"mentra-3.1.0-dev.17\",\n \"sourceCommit\": \"84c1e737fddc23bf9abb9b1916937c5b7e892f54\",\n \"otaManifestUrl\": \"https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.17/mentra-live-ota-3.1.0-dev.17.json\",\n \"otaManifestSha256\": \"47e576dec6afd4cc6ccbc758b12fe65b33b180382a899f8098dfcd11f7bcb2a6\"\n})\n"]}
@@ -11,7 +11,7 @@ enum BluetoothSdkDefaults {
11
11
  static let voiceActivityDetectionEnabled = false
12
12
  static let loudnessGateEnabled = true
13
13
  private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
14
- private static let swiftPackageSdkVersion = "3.1.0-dev.15"
14
+ private static let swiftPackageSdkVersion = "3.1.0-dev.17"
15
15
  private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
16
16
 
17
17
  private static func normalizedSdkVersion(_ value: String?) -> String? {
@@ -3,9 +3,9 @@ import Foundation
3
3
  /// Generated by release CI. Do not edit in a release checkout.
4
4
  enum GeneratedReleaseMetadata {
5
5
  static let familyBaseVersion = "3.1.0"
6
- static let releaseIdentity = "3.1.0-dev.15"
7
- static let releaseSetId = "mentra-3.1.0-dev.15"
8
- static let sourceCommit = "b22afd316787f99de5df15b323b08f5e8d6c40f5"
9
- static let otaManifestUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.15/mentra-live-ota-3.1.0-dev.15.json"
6
+ static let releaseIdentity = "3.1.0-dev.17"
7
+ static let releaseSetId = "mentra-3.1.0-dev.17"
8
+ static let sourceCommit = "84c1e737fddc23bf9abb9b1916937c5b7e892f54"
9
+ static let otaManifestUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.17/mentra-live-ota-3.1.0-dev.17.json"
10
10
  static let otaManifestSha256 = "47e576dec6afd4cc6ccbc758b12fe65b33b180382a899f8098dfcd11f7bcb2a6"
11
11
  }
@@ -311,6 +311,7 @@ public final class MentraBluetoothSDK {
311
311
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", device.projectName ?? "")
312
312
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
313
313
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
314
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
314
315
  finishDefaultDeviceApply(generation: generation)
315
316
  }
316
317
 
@@ -324,6 +325,7 @@ public final class MentraBluetoothSDK {
324
325
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", "")
325
326
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
326
327
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
328
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
327
329
  finishDefaultDeviceApply(generation: generation)
328
330
  }
329
331
 
@@ -401,6 +403,11 @@ public final class MentraBluetoothSDK {
401
403
  } else if !isController {
402
404
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", device.name)
403
405
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", device.identifier ?? "")
406
+ DeviceStore.shared.apply(
407
+ ObservableStore.bluetoothCategory,
408
+ "pending_device_secure_pairing_capable",
409
+ device.securePairingCapable ?? ""
410
+ )
404
411
  }
405
412
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_wearable", device.model.deviceType)
406
413
  DeviceManager.shared.connectByName(device.name)
@@ -427,6 +434,7 @@ public final class MentraBluetoothSDK {
427
434
  clearBluetoothRestoreIntent()
428
435
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
429
436
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
437
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
430
438
  DeviceManager.shared.disconnect()
431
439
  }
432
440
 
@@ -439,6 +447,7 @@ public final class MentraBluetoothSDK {
439
447
  clearBluetoothRestoreIntent()
440
448
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
441
449
  DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
450
+ DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
442
451
  DeviceManager.shared.disconnect()
443
452
  }
444
453
 
@@ -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 {
@@ -967,6 +996,7 @@ extension MentraLive: CBCentralManagerDelegate {
967
996
  self.isConnecting = false
968
997
  self.connectingPeripheral = nil
969
998
  self.connectedPeripheral = peripheral
999
+ self.emitConnectedPendingDeviceForPairingScan()
970
1000
 
971
1001
  // Save device name and address for future reconnection
972
1002
  if let name = peripheral.name {
@@ -1618,6 +1648,7 @@ class MentraLive: NSObject, SGCManager {
1618
1648
 
1619
1649
  // State Tracking
1620
1650
  private var isScanning = false
1651
+ private var manualDiscoveryActive = false
1621
1652
  private var isConnecting = false
1622
1653
  private var isKilled = false
1623
1654
  /// Glasses opened pairing window — stand down without forgetting identity/bonds.
@@ -1772,7 +1803,9 @@ class MentraLive: NSObject, SGCManager {
1772
1803
  // clear the saved device name:
1773
1804
  UserDefaults.standard.set("", forKey: PREFS_DEVICE_NAME)
1774
1805
 
1806
+ manualDiscoveryActive = true
1775
1807
  startScan()
1808
+ emitConnectedPendingDeviceForPairingScan()
1776
1809
  }
1777
1810
  }
1778
1811
 
@@ -2364,10 +2397,6 @@ class MentraLive: NSObject, SGCManager {
2364
2397
 
2365
2398
  centralManager?.scanForPeripherals(withServices: nil, options: scanOptions)
2366
2399
 
2367
- // Fresh advertisements refill pairing metadata. Re-emitting the last scan's
2368
- // cache would keep a unit pairable after it left pairing mode.
2369
- emitConnectedDeviceForPairingScan()
2370
-
2371
2400
  // var dName = DeviceManager.shared.deviceName
2372
2401
  // if dName.isEmpty {
2373
2402
  // dName = "MENTRA_LIVE"
@@ -2385,6 +2414,7 @@ class MentraLive: NSObject, SGCManager {
2385
2414
  }
2386
2415
 
2387
2416
  func stopScan() {
2417
+ manualDiscoveryActive = false
2388
2418
  guard isScanning else { return }
2389
2419
 
2390
2420
  centralManager?.stopScan()
@@ -5502,22 +5532,33 @@ class MentraLive: NSObject, SGCManager {
5502
5532
 
5503
5533
  // MARK: - Event Emission
5504
5534
 
5505
- /// Pairing scan listens for advertisements. A unit that is already GATT-connected
5506
- /// has stopped ADV, so emit it as pairable or the scan list stays empty.
5507
- private func emitConnectedDeviceForPairingScan() {
5508
- guard connected, let peripheral = connectedPeripheral, let name = peripheral.name,
5509
- name == "Xy_A" || name.hasPrefix("XyBLE_") || name.hasPrefix("MENTRA_LIVE_BLE")
5510
- || name.hasPrefix("MENTRA_LIVE_BT") || name.lowercased().hasPrefix("mentra_live")
5535
+ /// A selected pairing target can stop advertising once GATT connects, before readiness
5536
+ /// promotes it to the default device. Re-emit only that explicit pending target; an
5537
+ /// established owner's connected glasses have no pending identity and remain hidden.
5538
+ private func emitConnectedPendingDeviceForPairingScan() {
5539
+ guard manualDiscoveryActive, !pairingYieldActive, let peripheral = connectedPeripheral,
5540
+ let name = peripheral.name,
5541
+ MentraLivePendingPairingTarget.shouldRecover(
5542
+ isConnected: peripheral.state == .connected,
5543
+ connectedName: name,
5544
+ connectedIdentifier: peripheral.identifier.uuidString,
5545
+ pendingName: DeviceStore.shared.get("bluetooth", "pending_device_name") as? String ?? "",
5546
+ pendingIdentifier: DeviceStore.shared.get("bluetooth", "pending_device_address") as? String ?? ""
5547
+ )
5511
5548
  else {
5512
5549
  return
5513
5550
  }
5514
- Bridge.log("LIVE: Pairing scan: already GATT-connected to \(name) — emitting as pairable (ADV off while connected)")
5551
+
5552
+ Bridge.log("LIVE: Pairing scan: recovering connected pending target \(name) (\(peripheral.identifier))")
5515
5553
  emitDiscoveredDevice(
5516
5554
  name,
5517
5555
  identifier: peripheral.identifier.uuidString,
5518
5556
  pairingMode: true,
5519
5557
  pairingCode: nil,
5520
- securePairingCapable: discoveredAdvPairing[name]?.securePairingCapable ?? false
5558
+ securePairingCapable: DeviceStore.shared.get(
5559
+ "bluetooth",
5560
+ "pending_device_secure_pairing_capable"
5561
+ ) as? Bool
5521
5562
  )
5522
5563
  }
5523
5564
 
@@ -3,6 +3,62 @@ import Foundation
3
3
  import XCTest
4
4
 
5
5
  final class MentraLivePairingAdvertisementTests: XCTestCase {
6
+ func testConnectedPairingRecoveryRequiresExplicitPendingTarget() {
7
+ XCTAssertFalse(
8
+ MentraLivePendingPairingTarget.matches(
9
+ connectedName: "MENTRA_LIVE_BLE_OWNER",
10
+ connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
11
+ pendingName: "",
12
+ pendingIdentifier: ""
13
+ )
14
+ )
15
+ XCTAssertTrue(
16
+ MentraLivePendingPairingTarget.matches(
17
+ connectedName: "MENTRA_LIVE_BLE_TARGET",
18
+ connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
19
+ pendingName: "MENTRA_LIVE_BLE_TARGET",
20
+ pendingIdentifier: ""
21
+ )
22
+ )
23
+ XCTAssertTrue(
24
+ MentraLivePendingPairingTarget.matches(
25
+ connectedName: "MENTRA_LIVE_BLE_TARGET",
26
+ connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
27
+ pendingName: "OTHER",
28
+ pendingIdentifier: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
29
+ )
30
+ )
31
+ XCTAssertFalse(
32
+ MentraLivePendingPairingTarget.matches(
33
+ connectedName: "MENTRA_LIVE_BLE_TARGET",
34
+ connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
35
+ pendingName: "MENTRA_LIVE_BLE_TARGET",
36
+ pendingIdentifier: "11111111-2222-3333-4444-555555555555"
37
+ )
38
+ )
39
+ }
40
+
41
+ func testConnectedPairingRecoveryRequiresActiveGattConnection() {
42
+ XCTAssertFalse(
43
+ MentraLivePendingPairingTarget.shouldRecover(
44
+ isConnected: false,
45
+ connectedName: "MENTRA_LIVE_BLE_TARGET",
46
+ connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
47
+ pendingName: "MENTRA_LIVE_BLE_TARGET",
48
+ pendingIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"
49
+ )
50
+ )
51
+ XCTAssertTrue(
52
+ MentraLivePendingPairingTarget.shouldRecover(
53
+ isConnected: true,
54
+ connectedName: "MENTRA_LIVE_BLE_TARGET",
55
+ connectedIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
56
+ pendingName: "MENTRA_LIVE_BLE_TARGET",
57
+ pendingIdentifier: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"
58
+ )
59
+ )
60
+ }
61
+
6
62
  func testConnectionCallbackPolicyRejectsPairingYieldAndStaleAttempts() {
7
63
  XCTAssertFalse(
8
64
  MentraLiveConnectionAttemptPolicy.shouldAcceptDidConnect(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "3.1.0-dev.15",
3
+ "version": "3.1.0-dev.17",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
12
12
  export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.1.0",
15
- "releaseIdentity": "3.1.0-dev.15",
16
- "releaseSetId": "mentra-3.1.0-dev.15",
17
- "sourceCommit": "b22afd316787f99de5df15b323b08f5e8d6c40f5",
18
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.15/mentra-live-ota-3.1.0-dev.15.json",
15
+ "releaseIdentity": "3.1.0-dev.17",
16
+ "releaseSetId": "mentra-3.1.0-dev.17",
17
+ "sourceCommit": "84c1e737fddc23bf9abb9b1916937c5b7e892f54",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.17/mentra-live-ota-3.1.0-dev.17.json",
19
19
  "otaManifestSha256": "47e576dec6afd4cc6ccbc758b12fe65b33b180382a899f8098dfcd11f7bcb2a6"
20
20
  })