@mentra/bluetooth-sdk 3.1.0-dev.7 → 3.1.0-dev.70
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 +40 -34
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +0 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +13 -8
- 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 +81 -69
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
- package/build/BluetoothSdk.types.d.ts +26 -5
- 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/Bridge.swift +1 -6
- package/ios/Source/DeviceManager.swift +13 -8
- 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 +93 -80
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -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 -5
- 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/mentra-live/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,39 +364,34 @@ 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.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
Bluetooth SDK releases also publish
|
|
390
|
-
`bluetooth-sdk-<sdkVersion>-ota-bundle.zip` beside the default manifest. The archive contains
|
|
391
|
-
`version.template.json`, every referenced ASG/MTK/BES artifact, `SHA256SUMS`, and a dependency-free
|
|
392
|
-
configuration script. After unpacking it, generate the final manifest for its exact hosting URL:
|
|
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.
|
|
370
|
+
|
|
371
|
+
Release CI embeds an immutable manifest URL and checksum into every published
|
|
372
|
+
SDK distribution. The completed coordinated release record correlates the SDK
|
|
373
|
+
package with its exact manifest and OTA bundle. A completed release is available at
|
|
374
|
+
`https://github.com/Mentra-Community/MentraOS/releases/tag/mentra-v<releaseIdentity>`
|
|
375
|
+
and contains `mentra-release-<releaseIdentity>.json`, which records the exact
|
|
376
|
+
package coordinates, asset URLs, sizes, checksums, and build provenance.
|
|
377
|
+
|
|
378
|
+
Pre-wall-clock ASG builds that ignore `ota_start.ota_version_url` are checked
|
|
379
|
+
against the URL they advertise, or the production default if they do not
|
|
380
|
+
advertise one, so the app does not prompt for an update the glasses cannot
|
|
381
|
+
install.
|
|
382
|
+
|
|
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.
|
|
387
|
+
|
|
388
|
+
Each coordinated prerelease publishes a portable OTA bundle. Stable releases
|
|
389
|
+
promote the exact beta-tested OTA bytes, so always resolve the bundle coordinate
|
|
390
|
+
and URL from `mentra-release-<releaseIdentity>.json` rather than constructing a
|
|
391
|
+
filename. The selected archive contains
|
|
392
|
+
`version.template.json`, every referenced ASG/MTK/BES artifact, `SHA256SUMS`,
|
|
393
|
+
and a dependency-free configuration script. After unpacking it, generate the
|
|
394
|
+
final manifest for its exact hosting URL:
|
|
393
395
|
|
|
394
396
|
```sh
|
|
395
397
|
node configure.mjs https://updates.example.internal/mentra-live/version.json
|
|
@@ -405,6 +407,10 @@ automatically.
|
|
|
405
407
|
|
|
406
408
|
OTA requires Mentra Live glasses firmware that supports the ASG OTA protocol and network access from the glasses. During install, normal BLE traffic can be interrupted and the glasses may restart; keep the app connected and avoid sending unrelated commands until `ota_status.status` is `complete` or `failed`.
|
|
407
409
|
|
|
410
|
+
For release selection, bundle verification, analytics opt-out, internal server
|
|
411
|
+
requirements, and disconnected validation, see the
|
|
412
|
+
[air-gapped deployment guide](https://docs.mentraglass.com/mentra-live/air-gapped-deployment).
|
|
413
|
+
|
|
408
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.
|
|
409
415
|
|
|
410
416
|
## Photo Upload
|
|
@@ -148,21 +148,17 @@ public class Bridge private constructor() {
|
|
|
148
148
|
@JvmStatic
|
|
149
149
|
fun sendPairingInfo(
|
|
150
150
|
hadPreviousBond: Boolean,
|
|
151
|
-
transferId: String? = null,
|
|
152
151
|
pairingCode: String? = null,
|
|
153
152
|
classicBondReady: Boolean = false,
|
|
154
153
|
securePairingCapable: Boolean = true,
|
|
155
154
|
protocolVersion: Int = 1,
|
|
156
|
-
binding: String? = null,
|
|
157
155
|
) {
|
|
158
156
|
val data = HashMap<String, Any>()
|
|
159
157
|
data["had_previous_bond"] = hadPreviousBond
|
|
160
|
-
if (transferId != null) data["transfer_id"] = transferId
|
|
161
158
|
if (pairingCode != null) data["pairing_code"] = pairingCode
|
|
162
159
|
data["classic_bond_ready"] = classicBondReady
|
|
163
160
|
data["secure_pairing_capable"] = securePairingCapable
|
|
164
161
|
data["protocol_version"] = protocolVersion
|
|
165
|
-
if (binding != null) data["binding"] = binding
|
|
166
162
|
sendTypedMessage("pairing_info", data as Map<String, Any>)
|
|
167
163
|
}
|
|
168
164
|
|
|
@@ -912,4 +908,3 @@ public class Bridge private constructor() {
|
|
|
912
908
|
}
|
|
913
909
|
}
|
|
914
910
|
}
|
|
915
|
-
|
|
@@ -1359,9 +1359,7 @@ class DeviceManager {
|
|
|
1359
1359
|
if (pendingDeviceAddress.isNotEmpty()) {
|
|
1360
1360
|
deviceAddress = pendingDeviceAddress
|
|
1361
1361
|
}
|
|
1362
|
-
|
|
1363
|
-
pendingDeviceAddress = ""
|
|
1364
|
-
pendingWearable = ""
|
|
1362
|
+
clearPendingConnection()
|
|
1365
1363
|
|
|
1366
1364
|
val readyKey = "${sgc?.type}:${deviceName}"
|
|
1367
1365
|
val now = System.currentTimeMillis()
|
|
@@ -2137,7 +2135,7 @@ class DeviceManager {
|
|
|
2137
2135
|
return
|
|
2138
2136
|
}
|
|
2139
2137
|
|
|
2140
|
-
disconnect()
|
|
2138
|
+
disconnect(clearPendingIdentity = false)
|
|
2141
2139
|
Thread.sleep(100)
|
|
2142
2140
|
searching = true
|
|
2143
2141
|
pendingDeviceName = name
|
|
@@ -2170,7 +2168,7 @@ class DeviceManager {
|
|
|
2170
2168
|
handleDeviceReady()
|
|
2171
2169
|
}
|
|
2172
2170
|
|
|
2173
|
-
fun disconnect() {
|
|
2171
|
+
fun disconnect(clearPendingIdentity: Boolean = true) {
|
|
2174
2172
|
sgc?.clearDisplay()
|
|
2175
2173
|
sgc?.disconnect()
|
|
2176
2174
|
sgc = null // Clear the SGC reference after disconnect
|
|
@@ -2202,6 +2200,15 @@ class DeviceManager {
|
|
|
2202
2200
|
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
2203
2201
|
controller?.disconnect()
|
|
2204
2202
|
controller = null
|
|
2203
|
+
if (clearPendingIdentity) {
|
|
2204
|
+
clearPendingConnection()
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
private fun clearPendingConnection() {
|
|
2209
|
+
pendingDeviceName = ""
|
|
2210
|
+
pendingDeviceAddress = ""
|
|
2211
|
+
pendingWearable = ""
|
|
2205
2212
|
}
|
|
2206
2213
|
|
|
2207
2214
|
fun disconnectController() {
|
|
@@ -2276,9 +2283,7 @@ class DeviceManager {
|
|
|
2276
2283
|
defaultWearable = ""
|
|
2277
2284
|
deviceName = ""
|
|
2278
2285
|
deviceAddress = ""
|
|
2279
|
-
|
|
2280
|
-
pendingDeviceAddress = ""
|
|
2281
|
-
pendingWearable = ""
|
|
2286
|
+
clearPendingConnection()
|
|
2282
2287
|
Bridge.saveSetting("default_wearable", "")
|
|
2283
2288
|
Bridge.saveSetting("device_name", "")
|
|
2284
2289
|
Bridge.saveSetting("device_address", "")
|
|
@@ -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.70"
|
|
7
|
+
const val RELEASE_SET_ID: String = "mentra-3.1.0-dev.70"
|
|
8
|
+
const val SOURCE_COMMIT: String = "1554856d69f4cd5d14c1ea769171a444d3e0c4b4"
|
|
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.70.json"
|
|
10
|
+
const val OTA_MANIFEST_SHA256: String = "593990d9b25a7a47dd80c6a7e3486964b4349191a7abe885fcc24135e1604c69"
|
|
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
|
|
@@ -159,59 +186,25 @@ class MentraLive : SGCManager() {
|
|
|
159
186
|
10_000L // 10s before first scan after shutdown
|
|
160
187
|
|
|
161
188
|
private const val MENTRA_MANUFACTURER_ID = 0xB822
|
|
162
|
-
private const val ADV_MANUF_PAIRING_FLAG_OFFSET = 5
|
|
163
|
-
private const val ADV_PAIRING_DISCOVERABLE: Byte = 0x01
|
|
164
|
-
private const val ADV_PAIRING_PROTOCOL_VERSION_MIN = 1
|
|
165
|
-
private const val ADV_PAIRING_PROTOCOL_VERSION_MAX = 15
|
|
166
189
|
|
|
167
190
|
private fun manufacturerData(result: ScanResult): ByteArray? {
|
|
168
191
|
return result.scanRecord?.getManufacturerSpecificData(MENTRA_MANUFACTURER_ID)
|
|
169
192
|
}
|
|
170
193
|
|
|
171
|
-
/**
|
|
172
|
-
* OS-1615 ads append `flag | version | capability | code_lo | code_hi` after the
|
|
173
|
-
* connected byte. Field firmware uses the same 0xB822 company id but then writes the
|
|
174
|
-
* XOR'd Classic MAC at those offsets. Length alone is not a pairing flag: treating MAC
|
|
175
|
-
* bytes as version/capability hides every old unit as "secure, not in pairing mode".
|
|
176
|
-
*/
|
|
177
|
-
private fun hasSecurePairingTrailer(manufData: ByteArray?): Boolean {
|
|
178
|
-
if (manufData == null) {
|
|
179
|
-
return false
|
|
180
|
-
}
|
|
181
|
-
val trailerBase = ADV_MANUF_PAIRING_FLAG_OFFSET + 1
|
|
182
|
-
if (manufData.size < trailerBase + 4) {
|
|
183
|
-
return false
|
|
184
|
-
}
|
|
185
|
-
val version = manufData[trailerBase].toInt() and 0xFF
|
|
186
|
-
val capability = manufData[trailerBase + 1].toInt() and 0xFF
|
|
187
|
-
return version in ADV_PAIRING_PROTOCOL_VERSION_MIN..ADV_PAIRING_PROTOCOL_VERSION_MAX &&
|
|
188
|
-
(capability and 0x01) != 0
|
|
189
|
-
}
|
|
190
|
-
|
|
191
194
|
private fun isPairingDiscoverable(result: ScanResult): Boolean {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return false
|
|
195
|
-
}
|
|
196
|
-
return manufData[ADV_MANUF_PAIRING_FLAG_OFFSET] == ADV_PAIRING_DISCOVERABLE
|
|
195
|
+
return MentraLivePairingAdvertisementParser.parse(manufacturerData(result))?.pairingMode ==
|
|
196
|
+
true
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
private fun advertisesPairingFlag(result: ScanResult): Boolean {
|
|
200
|
-
return
|
|
200
|
+
return MentraLivePairingAdvertisementParser.parse(manufacturerData(result)) != null
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
/** Trailer immediately after pairing flag: version | capability | code_lo | code_hi */
|
|
204
203
|
private fun parseSecurePairingTrailer(result: ScanResult): Triple<Boolean, String?, Boolean> {
|
|
205
|
-
val
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
val pairingMode = manufData[ADV_MANUF_PAIRING_FLAG_OFFSET] == ADV_PAIRING_DISCOVERABLE
|
|
210
|
-
val trailerBase = ADV_MANUF_PAIRING_FLAG_OFFSET + 1
|
|
211
|
-
val codeLo = manufData[trailerBase + 2].toInt() and 0xFF
|
|
212
|
-
val codeHi = manufData[trailerBase + 3].toInt() and 0xFF
|
|
213
|
-
val code = String.format("%02X%02X", codeHi, codeLo)
|
|
214
|
-
return Triple(pairingMode, code, true)
|
|
204
|
+
val advertisement =
|
|
205
|
+
MentraLivePairingAdvertisementParser.parse(manufacturerData(result))
|
|
206
|
+
?: return Triple(false, null, false)
|
|
207
|
+
return Triple(advertisement.pairingMode, advertisement.pairingCode, true)
|
|
215
208
|
}
|
|
216
209
|
|
|
217
210
|
fun isMentraLiveBluetoothName(name: String?): Boolean {
|
|
@@ -1299,11 +1292,6 @@ class MentraLive : SGCManager() {
|
|
|
1299
1292
|
val generation = ++scanGeneration
|
|
1300
1293
|
isScanning = true
|
|
1301
1294
|
bluetoothScanner!!.startScan(filters, settings, scanCallback)
|
|
1302
|
-
// Connected glasses stop advertising (BLE_CONNECTION_MAX=1). Pairing scan
|
|
1303
|
-
// would otherwise return empty even though GATT is already up.
|
|
1304
|
-
if (!isReconnecting) {
|
|
1305
|
-
handler.post { emitConnectedDeviceForPairingScan() }
|
|
1306
|
-
}
|
|
1307
1295
|
|
|
1308
1296
|
// Set a timeout to stop scanning
|
|
1309
1297
|
handler.postDelayed(
|
|
@@ -1386,29 +1374,44 @@ class MentraLive : SGCManager() {
|
|
|
1386
1374
|
}
|
|
1387
1375
|
|
|
1388
1376
|
/**
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
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.
|
|
1392
1380
|
*/
|
|
1393
|
-
private fun
|
|
1394
|
-
if (!
|
|
1395
|
-
return
|
|
1396
|
-
}
|
|
1381
|
+
private fun emitConnectedPendingDeviceForPairingScan() {
|
|
1382
|
+
if (!manualDiscoveryActive || isReconnecting || pairingYieldActive) return
|
|
1397
1383
|
val device = connectedDevice ?: bluetoothGatt?.device ?: return
|
|
1398
1384
|
val name = safeBondedDeviceName(device)
|
|
1399
|
-
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
|
+
) {
|
|
1400
1402
|
return
|
|
1401
1403
|
}
|
|
1404
|
+
|
|
1402
1405
|
Bridge.log(
|
|
1403
|
-
"LIVE: Pairing scan:
|
|
1406
|
+
"LIVE: Pairing scan: recovering connected pending target $name ($address)"
|
|
1404
1407
|
)
|
|
1405
1408
|
Bridge.sendDiscoveredDevice(
|
|
1406
1409
|
DeviceTypes.LIVE,
|
|
1407
1410
|
name,
|
|
1408
|
-
|
|
1411
|
+
address,
|
|
1409
1412
|
pairingMode = true,
|
|
1410
1413
|
pairingCode = null,
|
|
1411
|
-
securePairingCapable =
|
|
1414
|
+
securePairingCapable = pendingSecurePairingCapable,
|
|
1412
1415
|
)
|
|
1413
1416
|
}
|
|
1414
1417
|
|
|
@@ -1475,10 +1478,11 @@ class MentraLive : SGCManager() {
|
|
|
1475
1478
|
advertisesPairingFlag(result) &&
|
|
1476
1479
|
!isPairingDiscoverable(result)
|
|
1477
1480
|
) {
|
|
1478
|
-
// Tell RN a nearby unit is advertising but not pairable
|
|
1479
|
-
//
|
|
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.
|
|
1480
1484
|
Bridge.log(
|
|
1481
|
-
"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"
|
|
1482
1486
|
)
|
|
1483
1487
|
Bridge.sendDiscoveredDevice(
|
|
1484
1488
|
DeviceTypes.LIVE,
|
|
@@ -2109,6 +2113,7 @@ class MentraLive : SGCManager() {
|
|
|
2109
2113
|
isConnecting = false
|
|
2110
2114
|
isConnected = true
|
|
2111
2115
|
connectedDevice = gatt.device
|
|
2116
|
+
emitConnectedPendingDeviceForPairingScan()
|
|
2112
2117
|
|
|
2113
2118
|
DeviceStore.apply("glasses", "bluetoothName", connectedDevice!!.name)
|
|
2114
2119
|
// Persist MAC so reconnection can use direct GATT instead of scanning
|
|
@@ -3817,19 +3822,16 @@ class MentraLive : SGCManager() {
|
|
|
3817
3822
|
val body = HashMap<String, Any>()
|
|
3818
3823
|
body["window_ms"] = windowMs
|
|
3819
3824
|
body["reason"] = json.optString("reason", "user_gesture")
|
|
3820
|
-
if (json.has("txn")) body["txn"] = json.optInt("txn")
|
|
3821
3825
|
Bridge.sendTypedMessage("entering_pairing_mode", body)
|
|
3822
3826
|
}
|
|
3823
3827
|
"pairing_info" ->
|
|
3824
3828
|
Bridge.sendPairingInfo(
|
|
3825
3829
|
json.optBoolean("had_previous_bond", false),
|
|
3826
|
-
jsonNullableString(json, "transfer_id"),
|
|
3827
3830
|
jsonNullableString(json, "pairing_code"),
|
|
3828
3831
|
json.optBoolean("classic_bond_ready", false),
|
|
3829
3832
|
// Legacy firmware that omits this field is not secure-capable.
|
|
3830
3833
|
json.optBoolean("secure_pairing_capable", false),
|
|
3831
3834
|
json.optInt("protocol_version", 1),
|
|
3832
|
-
jsonNullableString(json, "binding"),
|
|
3833
3835
|
)
|
|
3834
3836
|
"imu_response",
|
|
3835
3837
|
"imu_stream_response",
|
|
@@ -5956,6 +5958,7 @@ class MentraLive : SGCManager() {
|
|
|
5956
5958
|
|
|
5957
5959
|
// Start scanning for BLE devices
|
|
5958
5960
|
startScan()
|
|
5961
|
+
handler.post { emitConnectedPendingDeviceForPairingScan() }
|
|
5959
5962
|
}
|
|
5960
5963
|
|
|
5961
5964
|
private fun releaseStaleClassicForDiscovery() {
|
|
@@ -6852,12 +6855,21 @@ class MentraLive : SGCManager() {
|
|
|
6852
6855
|
")"
|
|
6853
6856
|
)
|
|
6854
6857
|
markPairingTiming("ctkd_initiate", "bondState=${device.bondState}")
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
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)
|
|
6861
6873
|
}
|
|
6862
6874
|
}
|
|
6863
6875
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
internal data class MentraLivePairingAdvertisement(
|
|
4
|
+
val pairingMode: Boolean,
|
|
5
|
+
val pairingCode: String,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
internal object MentraLivePairingAdvertisementParser {
|
|
9
|
+
private const val PAIRING_FLAG_OFFSET = 5
|
|
10
|
+
private const val PAIRING_DISCOVERABLE: Byte = 0x01
|
|
11
|
+
private const val PROTOCOL_VERSION_OFFSET = PAIRING_FLAG_OFFSET + 1
|
|
12
|
+
private const val CAPABILITY_OFFSET = PROTOCOL_VERSION_OFFSET + 1
|
|
13
|
+
private const val CODE_LOW_OFFSET = CAPABILITY_OFFSET + 1
|
|
14
|
+
private const val CODE_HIGH_OFFSET = CODE_LOW_OFFSET + 1
|
|
15
|
+
private const val MAGIC_FIRST_OFFSET = CODE_HIGH_OFFSET + 1
|
|
16
|
+
private const val MAGIC_SECOND_OFFSET = MAGIC_FIRST_OFFSET + 1
|
|
17
|
+
private const val PROTOCOL_VERSION_MIN = 2
|
|
18
|
+
private const val PROTOCOL_VERSION_MAX = 15
|
|
19
|
+
private const val SECURE_PAIRING_CAPABILITY = 0x01
|
|
20
|
+
private const val MAGIC_FIRST = 0x4D // M
|
|
21
|
+
private const val MAGIC_SECOND = 0x50 // P
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses company-id-stripped 0xB822 manufacturer data.
|
|
25
|
+
*
|
|
26
|
+
* Legacy firmware stores an XOR'd Classic MAC where the original pairing implementation
|
|
27
|
+
* expected version and capability bytes. Requiring the `MP` marker makes the formats
|
|
28
|
+
* unambiguous instead of probabilistically classifying MAC bytes as a secure trailer.
|
|
29
|
+
*/
|
|
30
|
+
fun parse(manufacturerData: ByteArray?): MentraLivePairingAdvertisement? {
|
|
31
|
+
if (manufacturerData == null || manufacturerData.size <= MAGIC_SECOND_OFFSET) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
val version = manufacturerData[PROTOCOL_VERSION_OFFSET].toInt() and 0xFF
|
|
36
|
+
val capability = manufacturerData[CAPABILITY_OFFSET].toInt() and 0xFF
|
|
37
|
+
val magicFirst = manufacturerData[MAGIC_FIRST_OFFSET].toInt() and 0xFF
|
|
38
|
+
val magicSecond = manufacturerData[MAGIC_SECOND_OFFSET].toInt() and 0xFF
|
|
39
|
+
if (version !in PROTOCOL_VERSION_MIN..PROTOCOL_VERSION_MAX ||
|
|
40
|
+
(capability and SECURE_PAIRING_CAPABILITY) == 0 ||
|
|
41
|
+
magicFirst != MAGIC_FIRST ||
|
|
42
|
+
magicSecond != MAGIC_SECOND
|
|
43
|
+
) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
val codeLow = manufacturerData[CODE_LOW_OFFSET].toInt() and 0xFF
|
|
48
|
+
val codeHigh = manufacturerData[CODE_HIGH_OFFSET].toInt() and 0xFF
|
|
49
|
+
return MentraLivePairingAdvertisement(
|
|
50
|
+
pairingMode = manufacturerData[PAIRING_FLAG_OFFSET] == PAIRING_DISCOVERABLE,
|
|
51
|
+
pairingCode = String.format("%02X%02X", codeHigh, codeLow),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -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
|
+
}
|