@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.
- package/README.md +16 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +15 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ReleaseChangelog.kt +61 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +74 -25
- package/android/src/test/java/com/mentra/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +58 -0
- package/build/BluetoothSdk.types.d.ts +26 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/changelogs.d.ts +7 -0
- package/build/changelogs.d.ts.map +1 -0
- package/build/changelogs.js +44 -0
- package/build/changelogs.js.map +1 -0
- package/build/generated/changelogCatalog.d.ts +6 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -0
- package/build/generated/changelogCatalog.js +8 -0
- package/build/generated/changelogCatalog.js.map +1 -0
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -0
- package/build/index.js.map +1 -1
- package/build/ota-transport/index.d.ts +67 -0
- package/build/ota-transport/index.d.ts.map +1 -0
- package/build/ota-transport/index.js +58 -0
- package/build/ota-transport/index.js.map +1 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedChangelogCatalog.swift +6 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +17 -0
- package/ios/Source/ReleaseChangelog.swift +65 -0
- package/ios/Source/sgcs/MentraLive.swift +56 -14
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +56 -0
- package/ios/Tests/ReleaseChangelogTests.swift +13 -0
- package/package.json +6 -1
- package/scripts/public-ota-api.test.mjs +37 -0
- package/src/BluetoothSdk.types.ts +28 -0
- package/src/changelogs.ts +44 -0
- package/src/generated/changelogCatalog.ts +9 -0
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +24 -0
- 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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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/
|
|
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.
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.1.0-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
9
|
-
const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-v3.1.0
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
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
|
-
*
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
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
|
|
1360
|
-
if (!
|
|
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:
|
|
1406
|
+
"LIVE: Pairing scan: recovering connected pending target $name ($address)"
|
|
1370
1407
|
)
|
|
1371
1408
|
Bridge.sendDiscoveredDevice(
|
|
1372
1409
|
DeviceTypes.LIVE,
|
|
1373
1410
|
name,
|
|
1374
|
-
|
|
1411
|
+
address,
|
|
1375
1412
|
pairingMode = true,
|
|
1376
1413
|
pairingCode = null,
|
|
1377
|
-
securePairingCapable =
|
|
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
|
|
1445
|
-
//
|
|
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 —
|
|
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
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
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;
|