@mentra/bluetooth-sdk 3.1.0-dev.10 → 3.1.0-dev.102

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 (45) hide show
  1. package/README.md +16 -21
  2. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +7 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  4. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +15 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/ReleaseChangelog.kt +61 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +74 -25
  7. package/android/src/test/java/com/mentra/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
  8. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +58 -0
  9. package/build/BluetoothSdk.types.d.ts +26 -0
  10. package/build/BluetoothSdk.types.d.ts.map +1 -1
  11. package/build/BluetoothSdk.types.js.map +1 -1
  12. package/build/changelogs.d.ts +7 -0
  13. package/build/changelogs.d.ts.map +1 -0
  14. package/build/changelogs.js +44 -0
  15. package/build/changelogs.js.map +1 -0
  16. package/build/generated/changelogCatalog.d.ts +6 -0
  17. package/build/generated/changelogCatalog.d.ts.map +1 -0
  18. package/build/generated/changelogCatalog.js +8 -0
  19. package/build/generated/changelogCatalog.js.map +1 -0
  20. package/build/generated/releaseMetadata.js +5 -5
  21. package/build/generated/releaseMetadata.js.map +1 -1
  22. package/build/index.d.ts +1 -1
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +14 -0
  25. package/build/index.js.map +1 -1
  26. package/build/ota-transport/index.d.ts +67 -0
  27. package/build/ota-transport/index.d.ts.map +1 -0
  28. package/build/ota-transport/index.js +58 -0
  29. package/build/ota-transport/index.js.map +1 -0
  30. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  31. package/ios/Source/GeneratedChangelogCatalog.swift +6 -0
  32. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  33. package/ios/Source/MentraBluetoothSDK.swift +17 -0
  34. package/ios/Source/ReleaseChangelog.swift +65 -0
  35. package/ios/Source/sgcs/MentraLive.swift +56 -14
  36. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +56 -0
  37. package/ios/Tests/ReleaseChangelogTests.swift +13 -0
  38. package/package.json +6 -1
  39. package/scripts/public-ota-api.test.mjs +37 -0
  40. package/src/BluetoothSdk.types.ts +28 -0
  41. package/src/changelogs.ts +44 -0
  42. package/src/generated/changelogCatalog.ts +9 -0
  43. package/src/generated/releaseMetadata.ts +5 -5
  44. package/src/index.ts +24 -0
  45. package/src/ota-transport/index.ts +116 -0
package/README.md CHANGED
@@ -349,7 +349,14 @@ Android and iOS async APIs use `BluetoothSdkException` / `BluetoothSdkError` for
349
349
 
350
350
  ## OTA Updates
351
351
 
352
- Mentra Live firmware owns the OTA flow. The SDK mirrors the MentraOS app commands and events:
352
+ React Native apps should use `MentraLiveOtaFlow` or `useMentraLiveOta` from
353
+ `@mentra/engine/ota`. They provide the same tested Wi-Fi/hotspot,
354
+ APK/MTK/BES, restart, retry, and verification flow as the Mentra App. See
355
+ [Update Mentra Live](https://docs.mentraglass.com/bluetooth-sdk/software-update).
356
+
357
+ The Bluetooth SDK exposes the lower-level commands and transport capabilities
358
+ that Mentra Engine and native apps build on. They are not a replacement for the
359
+ Engine coordinator:
353
360
 
354
361
  - `setOtaVersionUrl(url)` selects the manifest used by subsequent checks and installs. Use this
355
362
  for a customer-controlled internal update server.
@@ -357,6 +364,9 @@ Mentra Live firmware owns the OTA flow. The SDK mirrors the MentraOS app command
357
364
  when no override was supplied.
358
365
  - `checkForOtaUpdate()` fetches the configured manifest and resolves with `true` when an ASG APK, MTK, or BES update is available.
359
366
  - `startOtaUpdate()` sends `ota_start` with the same configured manifest URL and resolves with the ASG start ack after your app presents the update and the user accepts it.
367
+ - `queryOtaStatus()` returns the correlated status for the active session.
368
+ - `@mentra/bluetooth-sdk/ota-transport` exposes scoped hotspot networking and
369
+ the local OTA artifact server without exposing native module internals.
360
370
 
361
371
  Release CI embeds an immutable manifest URL and checksum into every published
362
372
  SDK distribution. The completed coordinated release record correlates the SDK
@@ -370,25 +380,10 @@ against the URL they advertise, or the production default if they do not
370
380
  advertise one, so the app does not prompt for an update the glasses cannot
371
381
  install.
372
382
 
373
- ```ts
374
- import BluetoothSdk from '@mentra/bluetooth-sdk'
375
-
376
- // Optional: point both the phone-side check and glasses install at an internal server.
377
- BluetoothSdk.setOtaVersionUrl('https://updates.example.internal/mentra-live/version.json')
378
-
379
- BluetoothSdk.addListener('ota_status', (event) => {
380
- console.log(`OTA ${event.status}: ${event.overall_percent}%`)
381
- })
382
-
383
- const hasUpdate = await BluetoothSdk.checkForOtaUpdate()
384
- if (hasUpdate) {
385
- const userAccepted = await promptUserToInstallUpdate() // your app's UI
386
- if (userAccepted) {
387
- const startAck = await BluetoothSdk.startOtaUpdate()
388
- console.log('OTA start acknowledged', startAck.timestamp)
389
- }
390
- }
391
- ```
383
+ Use these primitives directly only when implementing the documented native
384
+ Android/iOS OTA contract or infrastructure beneath a coordinator. React Native
385
+ application pages should render Mentra Engine's semantic controller state
386
+ instead of sequencing raw events.
392
387
 
393
388
  Each coordinated prerelease publishes a portable OTA bundle. Stable releases
394
389
  promote the exact beta-tested OTA bytes, so always resolve the bundle coordinate
@@ -414,7 +409,7 @@ OTA requires Mentra Live glasses firmware that supports the ASG OTA protocol and
414
409
 
415
410
  For release selection, bundle verification, analytics opt-out, internal server
416
411
  requirements, and disconnected validation, see the
417
- [air-gapped deployment guide](https://docs.mentraglass.com/mentra-live/air-gapped-deployment).
412
+ [air-gapped deployment guide](https://docs.mentraglass.com/bluetooth-sdk/air-gapped-deployment).
418
413
 
419
414
  Mentra Live also rejects `ota_start` before acknowledgement when its known battery level is below 5%, emitting a failed `ota_status` with `error_message: "battery_low"`. Unknown battery state remains fail-open, so apps should keep their own (typically stricter) user-facing battery policy.
420
415
 
@@ -0,0 +1,7 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ /** Generated from /changelogs. Do not edit directly. */
4
+ internal val GENERATED_RELEASE_CHANGELOGS: List<ReleaseChangelog> =
5
+ listOf(
6
+ ReleaseChangelog(version = "3.1.0", markdown = "Software updates are more reliable, with clearer progress and recovery when the glasses restart.\n\n- MentraOS, Mentra Engine, the Bluetooth SDK, and the glasses client now share one coordinated release version.\n- Mentra Live updates can continue across APK, system, and firmware restarts without asking the user to start the same update again.\n- Bluetooth photo capture and transfer diagnostics are more precise and less disruptive to normal glasses traffic."),
7
+ )
@@ -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.10"
7
- const val RELEASE_SET_ID: String = "mentra-3.1.0-dev.10"
8
- const val SOURCE_COMMIT: String = "64cf83b23a71e70261571a5f4401d1a7538f1852"
9
- const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0-dev.10/mentra-live-ota-3.1.0-dev.10.json"
10
- const val OTA_MANIFEST_SHA256: String = "214d92c8894033eaab276e668cb553c79990005d3330d23aaf499a514dc08cff"
6
+ const val RELEASE_IDENTITY: String = "3.1.0-dev.102"
7
+ const val RELEASE_SET_ID: String = "mentra-3.1.0-dev.102"
8
+ const val SOURCE_COMMIT: String = "1e5cd9f88a6a43db2f51f4ddd3c6a4b8f15d0fed"
9
+ const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.1.0/mentra-live-ota-3.1.0-dev.102.json"
10
+ const val OTA_MANIFEST_SHA256: String = "5ff3b609846566abecea90ce5fed4a8d281db2dee82a728b7d80a97beedc4f13"
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
 
@@ -1216,6 +1225,12 @@ class MentraBluetoothSdk private constructor(
1216
1225
  )
1217
1226
  }
1218
1227
 
1228
+ /** Return bundled release changelogs crossed between two coordinated product versions, newest first. */
1229
+ fun getReleaseChangelogs(
1230
+ fromVersion: String? = null,
1231
+ toVersion: String? = null,
1232
+ ): List<ReleaseChangelog> = ReleaseChangelogCatalog.select(fromVersion, toVersion)
1233
+
1219
1234
  /** Ask connected Mentra Live glasses to report the current OTA install/session status. */
1220
1235
  private suspend fun queryOtaStatus(): OtaQueryResult =
1221
1236
  performOtaQuery("OTA status query") {
@@ -0,0 +1,61 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ /** One coordinated Mentra release note authored in /changelogs/<version>.md. */
4
+ data class ReleaseChangelog(
5
+ val version: String,
6
+ val markdown: String,
7
+ )
8
+
9
+ internal object ReleaseChangelogCatalog {
10
+ private val baseVersionPattern = Regex("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:[-+].*)?$")
11
+
12
+ fun select(
13
+ fromVersion: String?,
14
+ toVersion: String?,
15
+ ): List<ReleaseChangelog> {
16
+ val fallbackTarget =
17
+ GeneratedReleaseMetadata.FAMILY_BASE_VERSION.ifBlank {
18
+ GENERATED_RELEASE_CHANGELOGS.firstOrNull()?.version.orEmpty()
19
+ }
20
+ if (fallbackTarget.isBlank() && toVersion == null) return emptyList()
21
+ val target = baseVersion(toVersion ?: fallbackTarget, "toVersion")
22
+ if (GENERATED_RELEASE_CHANGELOGS.none { it.version == target }) {
23
+ throw BluetoothSdkException("missing_changelog", "No changelog is bundled for target version $target.")
24
+ }
25
+ if (fromVersion == null) return GENERATED_RELEASE_CHANGELOGS.filter { it.version == target }
26
+
27
+ val source = baseVersion(fromVersion, "fromVersion")
28
+ val direction = compareVersions(target, source)
29
+ return GENERATED_RELEASE_CHANGELOGS.filter { entry ->
30
+ when {
31
+ direction == 0 -> entry.version == target
32
+ direction > 0 -> compareVersions(entry.version, source) > 0 && compareVersions(entry.version, target) <= 0
33
+ else -> compareVersions(entry.version, source) < 0 && compareVersions(entry.version, target) >= 0
34
+ }
35
+ }
36
+ }
37
+
38
+ private fun baseVersion(
39
+ version: String,
40
+ label: String,
41
+ ): String {
42
+ val match = baseVersionPattern.matchEntire(version.trim())
43
+ ?: throw BluetoothSdkException(
44
+ "invalid_changelog_version",
45
+ "$label must be a semantic version such as 3.1.0, 3.1.0-dev.4, or 3.1.0-beta.2.",
46
+ )
47
+ return "${match.groupValues[1]}.${match.groupValues[2]}.${match.groupValues[3]}"
48
+ }
49
+
50
+ private fun compareVersions(
51
+ left: String,
52
+ right: String,
53
+ ): Int {
54
+ val a = left.split(".").map(String::toInt)
55
+ val b = right.split(".").map(String::toInt)
56
+ for (index in 0..2) {
57
+ if (a[index] != b[index]) return a[index] - b[index]
58
+ }
59
+ return 0
60
+ }
61
+ }
@@ -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
 
@@ -1441,10 +1478,11 @@ class MentraLive : SGCManager() {
1441
1478
  advertisesPairingFlag(result) &&
1442
1479
  !isPairingDiscoverable(result)
1443
1480
  ) {
1444
- // Tell RN a nearby unit is advertising but not pairable so the
1445
- // empty-state hint can fire. The scan list never shows these.
1481
+ // Tell RN a nearby unit is advertising but not pairable. The scan
1482
+ // screen keeps it visible with pairing-mode guidance but blocks the
1483
+ // connection until a pairable advertisement arrives.
1446
1484
  Bridge.log(
1447
- "LIVE: Nearby $deviceName is secure firmware not in pairing mode — hiding from scan list"
1485
+ "LIVE: Nearby $deviceName is secure firmware not in pairing mode — exposing as non-pairable"
1448
1486
  )
1449
1487
  Bridge.sendDiscoveredDevice(
1450
1488
  DeviceTypes.LIVE,
@@ -2075,6 +2113,7 @@ class MentraLive : SGCManager() {
2075
2113
  isConnecting = false
2076
2114
  isConnected = true
2077
2115
  connectedDevice = gatt.device
2116
+ emitConnectedPendingDeviceForPairingScan()
2078
2117
 
2079
2118
  DeviceStore.apply("glasses", "bluetoothName", connectedDevice!!.name)
2080
2119
  // Persist MAC so reconnection can use direct GATT instead of scanning
@@ -5919,6 +5958,7 @@ class MentraLive : SGCManager() {
5919
5958
 
5920
5959
  // Start scanning for BLE devices
5921
5960
  startScan()
5961
+ handler.post { emitConnectedPendingDeviceForPairingScan() }
5922
5962
  }
5923
5963
 
5924
5964
  private fun releaseStaleClassicForDiscovery() {
@@ -6815,12 +6855,21 @@ class MentraLive : SGCManager() {
6815
6855
  ")"
6816
6856
  )
6817
6857
  markPairingTiming("ctkd_initiate", "bondState=${device.bondState}")
6818
- if (device.bondState == BluetoothDevice.BOND_BONDED) {
6819
- Bridge.log("LIVE: CTKD: Device is already bonded - connecting A2DP audio profile")
6820
- markPairingTiming("ctkd_already_bonded")
6821
- connectA2dpProfile(device)
6822
- } else {
6823
- createBond(device)
6858
+ when (device.bondState) {
6859
+ BluetoothDevice.BOND_BONDED -> {
6860
+ Bridge.log(
6861
+ "LIVE: CTKD: Device is already bonded - connecting A2DP audio profile"
6862
+ )
6863
+ markPairingTiming("ctkd_already_bonded")
6864
+ connectA2dpProfile(device)
6865
+ }
6866
+ BluetoothDevice.BOND_BONDING -> {
6867
+ // Secure pairing firmware may request SMP as soon as GATT connects.
6868
+ // Let that in-flight procedure finish instead of calling createBond twice.
6869
+ Bridge.log("LIVE: CTKD: Bonding already in progress - waiting for completion")
6870
+ markPairingTiming("ctkd_bonding_already_in_progress")
6871
+ }
6872
+ else -> createBond(device)
6824
6873
  }
6825
6874
  }
6826
6875
 
@@ -0,0 +1,13 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Test
5
+
6
+ class ReleaseChangelogTest {
7
+ @Test
8
+ fun includesTargetNotesForTransitionWithinOneReleaseTrain() {
9
+ val changelogs = ReleaseChangelogCatalog.select("3.1.0-dev.2", "3.1.0-beta.8")
10
+
11
+ assertEquals(listOf("3.1.0"), changelogs.map { it.version })
12
+ }
13
+ }
@@ -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))
@@ -875,6 +875,10 @@ export type BluetoothSdkEventMap = {
875
875
  mic_lc3: MicLc3Event;
876
876
  mic_health: MicHealthEvent;
877
877
  stream_status: StreamStatusEvent;
878
+ /** Mentra Live MTK updater completed and the glasses are about to restart. */
879
+ mtk_update_complete: MtkUpdateCompleteEvent;
880
+ /** The ASG process restarted while the BES kept the BLE connection alive. */
881
+ glasses_session_changed: GlassesSessionChangedEvent;
878
882
  ota_start_ack: OtaStartAckEvent;
879
883
  ota_status: OtaStatusEvent;
880
884
  ar99_ota_status: Ar99OtaStatusEvent;
@@ -889,6 +893,14 @@ export type BluetoothSdkSubscription = {
889
893
  export type BluetoothSdkEvent = BluetoothSdkEventMap[BluetoothSdkEventName];
890
894
  export interface BluetoothSdkPublicModule {
891
895
  addListener<EventName extends BluetoothSdkEventName>(eventName: EventName, listener: BluetoothSdkEventListener<EventName>): BluetoothSdkSubscription;
896
+ /** Read an immutable snapshot of the current glasses state. */
897
+ getGlassesStatus(): Promise<PublicGlassesStatus>;
898
+ /** Read an immutable snapshot of the phone Bluetooth adapter state. */
899
+ getBluetoothStatus(): Promise<PublicBluetoothStatus>;
900
+ /** Observe immutable glasses-state patches. Returns an unsubscribe function. */
901
+ subscribeGlassesStatus(listener: (changed: Partial<PublicGlassesStatus>) => void): () => void;
902
+ /** Observe immutable Bluetooth-state patches. Returns an unsubscribe function. */
903
+ subscribeBluetoothStatus(listener: (changed: Partial<PublicBluetoothStatus>) => void): () => void;
892
904
  getDefaultDevice(): Promise<Device | null>;
893
905
  setDefaultDevice(device: Device | null): Promise<void>;
894
906
  clearDefaultDevice(): Promise<void>;
@@ -908,10 +920,14 @@ export interface BluetoothSdkPublicModule {
908
920
  setHeadUpAngle(angleDegrees: number): Promise<void>;
909
921
  setImuEnabled(enabled: boolean): Promise<void>;
910
922
  setScreenDisabled(disabled: boolean): Promise<void>;
923
+ /** Keep legacy Mentra Live OTA sessions awake. Modern sessions normally do not require this. */
924
+ ping(): Promise<void>;
911
925
  requestWifiScan(): Promise<WifiSearchResult[]>;
912
926
  sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>;
913
927
  forgetWifiNetwork(ssid: string): Promise<WifiStatusChangeEvent>;
914
928
  setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>;
929
+ /** Set the glasses clock from the phone after an OTA clock-skew failure. */
930
+ setSystemTime(timestampMs: number): Promise<void>;
915
931
  /** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
916
932
  setWifiAdbState(enabled: boolean): Promise<void>;
917
933
  setGalleryModeEnabled(enabled: boolean): Promise<SettingsAckSuccessEvent>;
@@ -978,8 +994,12 @@ export interface BluetoothSdkPublicModule {
978
994
  getOtaVersionUrl(): string;
979
995
  /** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
980
996
  checkForOtaUpdate(): Promise<boolean>;
997
+ /** Return bundled release changelogs crossed between two coordinated product versions, newest first. */
998
+ getReleaseChangelogs(fromVersion?: string | null, toVersion?: string | null): ReleaseChangelog[];
981
999
  /** Start OTA from the configured or explicitly supplied manifest URL. */
982
1000
  startOtaUpdate(otaVersionUrl?: string | null): Promise<OtaStartAckEvent>;
1001
+ /** Query the active OTA session and return the correlated status response. */
1002
+ queryOtaStatus(): Promise<OtaQueryResult>;
983
1003
  startAr99OtaFromFile(path: string): Promise<boolean>;
984
1004
  cancelAr99Ota(): Promise<void>;
985
1005
  sendAr99FactoryReset(): Promise<void>;
@@ -1022,6 +1042,12 @@ export interface OtaUpdateInfo {
1022
1042
  /** True when the APK step installs an older build than the glasses currently run (exact-pin manifests only). */
1023
1043
  isDowngrade?: boolean;
1024
1044
  }
1045
+ export interface ReleaseChangelog {
1046
+ /** Base production version, for example `3.1.0`. */
1047
+ version: string;
1048
+ /** Markdown body authored in `/changelogs/<version>.md`. */
1049
+ markdown: string;
1050
+ }
1025
1051
  export interface OtaProgress {
1026
1052
  stage: OtaStage;
1027
1053
  status: OtaProgressStatus;