@mentra/bluetooth-sdk 3.2.1-dev.311 → 3.2.1
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 +4 -4
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +129 -25
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +101 -22
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +245 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
- package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +220 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
- package/build/BluetoothSdk.types.d.ts +13 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +3 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +2 -2
- package/build/generated/changelogCatalog.js +2 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +9 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +96 -63
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +18 -0
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/Ar99.swift +1 -2
- package/ios/Source/sgcs/G1.swift +68 -38
- package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
- package/ios/Source/sgcs/G2.swift +261 -247
- package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
- package/ios/Source/sgcs/Mach1.swift +19 -7
- package/ios/Source/sgcs/MentraLive.swift +201 -156
- package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
- package/ios/Source/sgcs/MentraNex.swift +8 -31
- package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
- package/ios/Source/sgcs/Nimo.swift +280 -408
- package/ios/Source/sgcs/NimoCanvasCodec.swift +269 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
- package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
- package/ios/Source/sgcs/SGCManager.swift +24 -0
- package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
- package/ios/Source/status/DeviceStatus.swift +1 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
- package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
- package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/ios/Tests/NimoDiscoveryTests.swift +79 -0
- package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
- package/ios/Tests/VersionInfoCommandTests.swift +53 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/package.json +2 -2
- package/plugin/build/withIos.d.ts +2 -0
- package/plugin/build/withIos.js +12 -1
- package/src/BluetoothSdk.types.ts +14 -0
- package/src/_private/BluetoothSdkModule.ts +8 -0
- package/src/generated/changelogCatalog.ts +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +1 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk.sgcs
|
|
2
2
|
|
|
3
|
+
import com.mentra.bluetoothsdk.utils.G2Text
|
|
3
4
|
import com.mentra.bluetoothsdk.PhotoRequest
|
|
4
5
|
import android.bluetooth.BluetoothAdapter
|
|
5
6
|
import android.bluetooth.BluetoothGatt
|
|
@@ -1363,17 +1364,18 @@ class G2 : SGCManager() {
|
|
|
1363
1364
|
// Map device names to serial numbers (populated from manufacturer data during scan)
|
|
1364
1365
|
private val deviceNameToSerialNumber = mutableMapOf<String, String>()
|
|
1365
1366
|
|
|
1366
|
-
//
|
|
1367
|
+
// Scope each side's address to the selected serial. Legacy global addresses have
|
|
1368
|
+
// no trustworthy owner, so the first connection after upgrading learns them by scan.
|
|
1367
1369
|
private var leftGlassAddress: String?
|
|
1368
1370
|
get() =
|
|
1369
1371
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1370
|
-
.getString(KEY_LEFT_ADDRESS, null)
|
|
1372
|
+
.getString("$KEY_LEFT_ADDRESS:$DEVICE_SEARCH_ID", null)
|
|
1371
1373
|
set(value) {
|
|
1372
1374
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1373
1375
|
.edit()
|
|
1374
1376
|
.apply {
|
|
1375
|
-
if (value != null) putString(KEY_LEFT_ADDRESS, value)
|
|
1376
|
-
else remove(KEY_LEFT_ADDRESS)
|
|
1377
|
+
if (value != null) putString("$KEY_LEFT_ADDRESS:$DEVICE_SEARCH_ID", value)
|
|
1378
|
+
else remove("$KEY_LEFT_ADDRESS:$DEVICE_SEARCH_ID")
|
|
1377
1379
|
}
|
|
1378
1380
|
.apply()
|
|
1379
1381
|
}
|
|
@@ -1381,13 +1383,13 @@ class G2 : SGCManager() {
|
|
|
1381
1383
|
private var rightGlassAddress: String?
|
|
1382
1384
|
get() =
|
|
1383
1385
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1384
|
-
.getString(KEY_RIGHT_ADDRESS, null)
|
|
1386
|
+
.getString("$KEY_RIGHT_ADDRESS:$DEVICE_SEARCH_ID", null)
|
|
1385
1387
|
set(value) {
|
|
1386
1388
|
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1387
1389
|
.edit()
|
|
1388
1390
|
.apply {
|
|
1389
|
-
if (value != null) putString(KEY_RIGHT_ADDRESS, value)
|
|
1390
|
-
else remove(KEY_RIGHT_ADDRESS)
|
|
1391
|
+
if (value != null) putString("$KEY_RIGHT_ADDRESS:$DEVICE_SEARCH_ID", value)
|
|
1392
|
+
else remove("$KEY_RIGHT_ADDRESS:$DEVICE_SEARCH_ID")
|
|
1391
1393
|
}
|
|
1392
1394
|
.apply()
|
|
1393
1395
|
}
|
|
@@ -1664,8 +1666,15 @@ class G2 : SGCManager() {
|
|
|
1664
1666
|
if (char == null || gatt == null) return
|
|
1665
1667
|
char.value = packet
|
|
1666
1668
|
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
+
// A rejected write never entered Android's queue. Retry on this dedicated
|
|
1670
|
+
// BLE worker before later commands (especially a page replacement) overtake it.
|
|
1671
|
+
var ok = false
|
|
1672
|
+
for (attempt in 0 until 5) {
|
|
1673
|
+
ok = gatt.writeCharacteristic(char)
|
|
1674
|
+
bgcapNoteWriteResult(ok)
|
|
1675
|
+
if (ok) break
|
|
1676
|
+
if (attempt < 4) Thread.sleep(BLE_PACKET_GAP_MS)
|
|
1677
|
+
}
|
|
1669
1678
|
if (leg != null) {
|
|
1670
1679
|
Bridge.log("G2/FILE: write $leg ${packet.size}B -> ${if (ok) "queued" else "DROPPED (stack busy)"}")
|
|
1671
1680
|
}
|
|
@@ -2190,7 +2199,7 @@ class G2 : SGCManager() {
|
|
|
2190
2199
|
val rBorderColor = borderColor ?: defaultTextBorderColor
|
|
2191
2200
|
val rBorderRadius = borderRadius ?: defaultTextBorderRadius
|
|
2192
2201
|
val rPaddingLength = paddingLength ?: defaultTextPaddingLength
|
|
2193
|
-
val content =
|
|
2202
|
+
val content = G2Text.containerContent(text)
|
|
2194
2203
|
|
|
2195
2204
|
// Pure state mutation: update the container's content and schedule its sends; the reconcile
|
|
2196
2205
|
// loop does the actual updateText writes. Reuse an existing container if the rect matches
|
|
@@ -2257,6 +2266,12 @@ class G2 : SGCManager() {
|
|
|
2257
2266
|
}
|
|
2258
2267
|
|
|
2259
2268
|
override fun clearDisplay() {
|
|
2269
|
+
// Expo invokes this on its worker queue; all container mutations must
|
|
2270
|
+
// share Main with reconcileDisplay and scene updates.
|
|
2271
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
2272
|
+
mainHandler.post { clearDisplay() }
|
|
2273
|
+
return
|
|
2274
|
+
}
|
|
2260
2275
|
Bridge.log("G2: clearDisplay()")
|
|
2261
2276
|
// Clear the text in place — do NOT shut down + rebuild the EvenHub page. Tearing the page
|
|
2262
2277
|
// down (rebuildPage) kills audio streaming AND triggers a firmware systemExit /
|
|
@@ -2266,9 +2281,22 @@ class G2 : SGCManager() {
|
|
|
2266
2281
|
// several seconds (incident 8164175a). Just blank the text + clear images; the reconcile
|
|
2267
2282
|
// loop pushes the blanked text on a live page, and a dead page is only resurrected for
|
|
2268
2283
|
// meaningful (non-blank) content, so a clear can't churn it back up.
|
|
2269
|
-
for (
|
|
2270
|
-
|
|
2271
|
-
|
|
2284
|
+
for (container in textContainers) {
|
|
2285
|
+
// Erase the firmware's existing text BEFORE forgetting its container.
|
|
2286
|
+
// A queued reconcile cannot do this after the positioned container is removed.
|
|
2287
|
+
// Cover the previous byte range, rather than replacing only its first character.
|
|
2288
|
+
val blank = " ".repeat(maxOf(1, container.content.toByteArray(Charsets.UTF_8).size))
|
|
2289
|
+
container.content = blank
|
|
2290
|
+
container.pendingSends = 1 + EVEN_HUB_RESEND_COUNT
|
|
2291
|
+
if (pageCreated) {
|
|
2292
|
+
val erase = EvenHubProto.updateTextMessage(
|
|
2293
|
+
containerID = container.id,
|
|
2294
|
+
contentOffset = 0,
|
|
2295
|
+
contentLength = blank.length,
|
|
2296
|
+
content = blank
|
|
2297
|
+
)
|
|
2298
|
+
repeat(1 + EVEN_HUB_RESEND_COUNT) { sendEvenHubCommand(erase) }
|
|
2299
|
+
}
|
|
2272
2300
|
}
|
|
2273
2301
|
for (i in imageContainers.indices) {
|
|
2274
2302
|
// The firmware still shows this container's image; emptying bmpData locally never reaches
|
|
@@ -2296,8 +2324,13 @@ class G2 : SGCManager() {
|
|
|
2296
2324
|
textContainers.removeAll { it.id in huskIds }
|
|
2297
2325
|
sceneTextByElement.entries.removeAll { it.value in huskIds }
|
|
2298
2326
|
sceneImageByElement.clear()
|
|
2299
|
-
|
|
2300
|
-
|
|
2327
|
+
// Replace the live page now, while still on Main. A shutdown followed by
|
|
2328
|
+
// reconcile cannot clear this case: reconcile intentionally skips blank pages,
|
|
2329
|
+
// leaving the firmware's old positioned text visible until new speech arrives.
|
|
2330
|
+
// Queue the blank replacement before any following miniapp frame, without
|
|
2331
|
+
// tearing down the page (or its microphone session).
|
|
2332
|
+
Bridge.log("G2: clearDisplay() — replacing page after removing ${huskIds.size} positioned container(s)")
|
|
2333
|
+
if (pageCreated) createPageWithContainers()
|
|
2301
2334
|
}
|
|
2302
2335
|
}
|
|
2303
2336
|
|
|
@@ -2344,6 +2377,15 @@ class G2 : SGCManager() {
|
|
|
2344
2377
|
return false
|
|
2345
2378
|
}
|
|
2346
2379
|
|
|
2380
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
2381
|
+
mainHandler.post { applyBitmap(rx, ry, rw, rh, bmpData) }
|
|
2382
|
+
return true
|
|
2383
|
+
}
|
|
2384
|
+
return applyBitmap(rx, ry, rw, rh, bmpData)
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
// Called only on Main, including the inline scene-frame path.
|
|
2388
|
+
private fun applyBitmap(rx: Int, ry: Int, rw: Int, rh: Int, bmpData: ByteArray): Boolean {
|
|
2347
2389
|
// Pure state mutation: update the target container's bytes and mark it dirty. The reconcile
|
|
2348
2390
|
// loop is the sole sender, so two displayBitmap calls can never overlap a sendImageData and
|
|
2349
2391
|
// clobber the single-slot image ACK — no lock needed. Reuse an existing container if the rect
|
|
@@ -2538,7 +2580,7 @@ class G2 : SGCManager() {
|
|
|
2538
2580
|
@Suppress("NAME_SHADOWING")
|
|
2539
2581
|
val height = minOf(maxOf(height, minTextContainerHeight), 288 - y)
|
|
2540
2582
|
run {
|
|
2541
|
-
val content =
|
|
2583
|
+
val content = G2Text.containerContent(text)
|
|
2542
2584
|
val existingId = sceneTextByElement[elementId]
|
|
2543
2585
|
if (existingId != null) {
|
|
2544
2586
|
val idx = textContainers.indexOfFirst { it.id == existingId }
|
|
@@ -2999,15 +3041,15 @@ class G2 : SGCManager() {
|
|
|
2999
3041
|
}
|
|
3000
3042
|
// Only settle the flag if it's still empty — a displayBitmap during the await would
|
|
3001
3043
|
// have set new bytes, so leave it dirty for the next pass to send the real image.
|
|
3002
|
-
val jj = imageContainers.indexOfFirst { it
|
|
3044
|
+
val jj = imageContainers.indexOfFirst { it === container }
|
|
3003
3045
|
if (jj >= 0 && imageContainers[jj].bmpData.isEmpty()) {
|
|
3004
3046
|
imageContainers[jj].dirty = false
|
|
3005
3047
|
}
|
|
3006
3048
|
continue
|
|
3007
3049
|
}
|
|
3008
3050
|
sendImageData(container.id, container.name, sentBytes)
|
|
3009
|
-
// Re-find by
|
|
3010
|
-
val j = imageContainers.indexOfFirst { it
|
|
3051
|
+
// Re-find by identity: eviction can reuse this ID during the await.
|
|
3052
|
+
val j = imageContainers.indexOfFirst { it === container }
|
|
3011
3053
|
if (j >= 0 && imageContainers[j].bmpData.contentEquals(sentBytes)) {
|
|
3012
3054
|
imageContainers[j].dirty = false
|
|
3013
3055
|
}
|
|
@@ -3677,8 +3719,17 @@ class G2 : SGCManager() {
|
|
|
3677
3719
|
rightAuthenticated = false
|
|
3678
3720
|
startupPageCreated = false
|
|
3679
3721
|
pageCreated = false
|
|
3680
|
-
|
|
3681
|
-
|
|
3722
|
+
// Keep GATT teardown synchronous for manager replacement, but never
|
|
3723
|
+
// remove containers underneath Main's reconcile/scene iteration.
|
|
3724
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
3725
|
+
imageContainers.clear()
|
|
3726
|
+
textContainers.clear()
|
|
3727
|
+
} else {
|
|
3728
|
+
mainHandler.post {
|
|
3729
|
+
imageContainers.clear()
|
|
3730
|
+
textContainers.clear()
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3682
3733
|
dashboardShowing = 0
|
|
3683
3734
|
dashboardOpening = false
|
|
3684
3735
|
heartbeatCounter = 0
|
|
@@ -141,6 +141,7 @@ class MentraLive : SGCManager() {
|
|
|
141
141
|
private const val LC3_FRAME_SIZE = 40
|
|
142
142
|
private const val VOICE_ACTIVITY_DETECTION_SWITCH_TYPE = 8
|
|
143
143
|
private const val LOUDNESS_GATE_SWITCH_TYPE = 10
|
|
144
|
+
private const val AUTO_POWER_OFF_SWITCH_TYPE = 11
|
|
144
145
|
// Mic tuning field names, matching the BES cs_mictun body.
|
|
145
146
|
private val MIC_TUNING_FIELDS =
|
|
146
147
|
listOf("gain", "open", "close", "attack", "hang", "sp_open", "sp_close", "sp_hold")
|
|
@@ -776,7 +777,8 @@ class MentraLive : SGCManager() {
|
|
|
776
777
|
}
|
|
777
778
|
|
|
778
779
|
// Inner class to track incoming file transfers
|
|
779
|
-
|
|
780
|
+
internal class FileTransferSession(var fileName: String, var fileSize: Int, flags: Int = 0) {
|
|
781
|
+
private val dynamicPayload = (flags and 0x0002) != 0
|
|
780
782
|
// NOTE: fileSize may be "fake" (inflated) due to BES firmware workaround
|
|
781
783
|
var actualPackSize =
|
|
782
784
|
0 // Actual pack size from first received packet (for BES lie detection)
|
|
@@ -805,9 +807,8 @@ class MentraLive : SGCManager() {
|
|
|
805
807
|
* Recalculate total packets based on actual pack size from received packet. Called when
|
|
806
808
|
* first packet is received to handle variable pack sizes.
|
|
807
809
|
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
* so, totalPackets = fileSize / 400, regardless of actual pack size.
|
|
810
|
+
* Dynamic-payload headers carry the true byte count. Only legacy small packs use
|
|
811
|
+
* the inflated-size workaround (multiples of BES_HARDCODED_PACK_SIZE).
|
|
811
812
|
*/
|
|
812
813
|
fun recalculateTotalPackets(actualPackSize: Int) {
|
|
813
814
|
if (actualPackSize <= 0 || actualPackSize > K900ProtocolUtils.FILE_PACK_SIZE) {
|
|
@@ -818,8 +819,8 @@ class MentraLive : SGCManager() {
|
|
|
818
819
|
|
|
819
820
|
// Detect BES lie: if fileSize is exact multiple of 400, glasses used the lie strategy
|
|
820
821
|
val isBesLie =
|
|
821
|
-
(fileSize % BES_HARDCODED_PACK_SIZE == 0) &&
|
|
822
|
-
(actualPackSize
|
|
822
|
+
!dynamicPayload && (fileSize % BES_HARDCODED_PACK_SIZE == 0) &&
|
|
823
|
+
(actualPackSize < BES_HARDCODED_PACK_SIZE)
|
|
823
824
|
|
|
824
825
|
val newTotalPackets: Int
|
|
825
826
|
if (isBesLie) {
|
|
@@ -857,6 +858,10 @@ class MentraLive : SGCManager() {
|
|
|
857
858
|
}
|
|
858
859
|
|
|
859
860
|
fun addPacket(index: Int, data: ByteArray): Boolean {
|
|
861
|
+
if (dynamicPayload && (index < 0 || index >= totalPackets || actualPackSize <= 0 ||
|
|
862
|
+
data.size != minOf(actualPackSize, fileSize - index * actualPackSize))) {
|
|
863
|
+
return false
|
|
864
|
+
}
|
|
860
865
|
if (index >= 0 && index < totalPackets && !receivedPackets.containsKey(index)) {
|
|
861
866
|
receivedPackets[index] = data
|
|
862
867
|
|
|
@@ -2147,7 +2152,7 @@ class MentraLive : SGCManager() {
|
|
|
2147
2152
|
// than scanning).
|
|
2148
2153
|
// Falls back to name-based scan if no address is saved.
|
|
2149
2154
|
val lastDeviceAddress =
|
|
2150
|
-
|
|
2155
|
+
selectedConnectionAddress()
|
|
2151
2156
|
if (lastDeviceAddress != null &&
|
|
2152
2157
|
!lastDeviceAddress.isEmpty() &&
|
|
2153
2158
|
bluetoothAdapter != null
|
|
@@ -4137,7 +4142,9 @@ class MentraLive : SGCManager() {
|
|
|
4137
4142
|
"controllerId" to json.opt("controllerId"), "streamId" to json.opt("streamId"),
|
|
4138
4143
|
"probeId" to json.opt("probeId"))
|
|
4139
4144
|
com.mentra.bluetoothsdk.streaming.StreamControllerProbe.response(values)?.let {
|
|
4140
|
-
|
|
4145
|
+
// BES buffers non-waking commands when MTK enters standby, even while
|
|
4146
|
+
// its streaming CPU lease is held. The current probe needs a live reply.
|
|
4147
|
+
sendJson(JSONObject(it), true)
|
|
4141
4148
|
}
|
|
4142
4149
|
}
|
|
4143
4150
|
"pong" ->
|
|
@@ -5548,7 +5555,9 @@ class MentraLive : SGCManager() {
|
|
|
5548
5555
|
if (bodyObj != null) {
|
|
5549
5556
|
val type = bodyObj.optInt("type", -1)
|
|
5550
5557
|
val value = bodyObj.optInt("switch", -1)
|
|
5551
|
-
|
|
5558
|
+
// K900 replies carry result in "S"; 0 is RC_SUCCESS.
|
|
5559
|
+
val resultCode = json.optInt("S", -1)
|
|
5560
|
+
handleSwitchStatus(type, value, System.currentTimeMillis(), resultCode)
|
|
5552
5561
|
}
|
|
5553
5562
|
} catch (e: Exception) {
|
|
5554
5563
|
Log.e(TAG, "Error parsing sr_swit response", e)
|
|
@@ -5578,7 +5587,13 @@ class MentraLive : SGCManager() {
|
|
|
5578
5587
|
try {
|
|
5579
5588
|
val bodyObj = optK900Body(json)
|
|
5580
5589
|
if (bodyObj != null) {
|
|
5581
|
-
|
|
5590
|
+
val extras = HashMap<String, Any>()
|
|
5591
|
+
if (bodyObj.has("elapsed_ms")) extras["elapsedMs"] = bodyObj.optInt("elapsed_ms")
|
|
5592
|
+
if (bodyObj.has("timeout_ms")) extras["timeoutMs"] = bodyObj.optInt("timeout_ms")
|
|
5593
|
+
if (bodyObj.has("enabled")) extras["enabled"] = bodyObj.optInt("enabled") != 0
|
|
5594
|
+
if (bodyObj.has("armed")) extras["armed"] = bodyObj.optInt("armed") != 0
|
|
5595
|
+
if (bodyObj.has("inhibited")) extras["inhibited"] = bodyObj.optInt("inhibited") != 0
|
|
5596
|
+
Bridge.sendWearState(bodyObj.optInt("on", 0) != 0, extras)
|
|
5582
5597
|
}
|
|
5583
5598
|
} catch (e: Exception) {
|
|
5584
5599
|
Log.e(TAG, "Error parsing sr_wrst response", e)
|
|
@@ -5933,8 +5948,20 @@ class MentraLive : SGCManager() {
|
|
|
5933
5948
|
Bridge.sendWearTuningState(state)
|
|
5934
5949
|
}
|
|
5935
5950
|
|
|
5936
|
-
private fun handleSwitchStatus(
|
|
5951
|
+
private fun handleSwitchStatus(
|
|
5952
|
+
switchType: Int,
|
|
5953
|
+
switchValue: Int,
|
|
5954
|
+
timestamp: Long,
|
|
5955
|
+
resultCode: Int = -1
|
|
5956
|
+
) {
|
|
5937
5957
|
Bridge.sendSwitchStatus(switchType, switchValue, timestamp)
|
|
5958
|
+
if (switchType == AUTO_POWER_OFF_SWITCH_TYPE) {
|
|
5959
|
+
val ok = resultCode == 0
|
|
5960
|
+
Bridge.log(
|
|
5961
|
+
"LIVE: 🔋 auto power-off sr_swit reply type=$switchType switch=$switchValue" +
|
|
5962
|
+
" result=$resultCode ok=$ok"
|
|
5963
|
+
)
|
|
5964
|
+
}
|
|
5938
5965
|
if (switchType == VOICE_ACTIVITY_DETECTION_SWITCH_TYPE &&
|
|
5939
5966
|
(switchValue == 0 || switchValue == 1)
|
|
5940
5967
|
) {
|
|
@@ -6140,7 +6167,8 @@ class MentraLive : SGCManager() {
|
|
|
6140
6167
|
val json = JSONObject()
|
|
6141
6168
|
json.put("type", "request_version")
|
|
6142
6169
|
requestId?.let { json.put("request_id", it) }
|
|
6143
|
-
|
|
6170
|
+
// Wake ASG so the version request and its response can finish after idle.
|
|
6171
|
+
sendJson(json, true)
|
|
6144
6172
|
Bridge.log("LIVE: 📱 Requesting version info from glasses")
|
|
6145
6173
|
} catch (e: JSONException) {
|
|
6146
6174
|
Log.e(TAG, "📱 Error creating request_version command", e)
|
|
@@ -6602,6 +6630,14 @@ class MentraLive : SGCManager() {
|
|
|
6602
6630
|
return false
|
|
6603
6631
|
}
|
|
6604
6632
|
|
|
6633
|
+
private fun selectedConnectionAddress(): String? = SelectedDeviceAddress.resolve(
|
|
6634
|
+
savedDeviceName,
|
|
6635
|
+
DeviceStore.get("bluetooth", "pending_device_name") as? String,
|
|
6636
|
+
DeviceStore.get("bluetooth", "pending_device_address") as? String,
|
|
6637
|
+
DeviceStore.get("bluetooth", "device_name") as? String,
|
|
6638
|
+
DeviceStore.get("bluetooth", "device_address") as? String,
|
|
6639
|
+
)
|
|
6640
|
+
|
|
6605
6641
|
fun connectToSmartGlasses() {
|
|
6606
6642
|
if (postGattLifecycle { connectToSmartGlasses() }) return
|
|
6607
6643
|
if (pairingYieldActive) {
|
|
@@ -6636,13 +6672,7 @@ class MentraLive : SGCManager() {
|
|
|
6636
6672
|
// var context = Bridge.getContext();
|
|
6637
6673
|
// SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
6638
6674
|
// String lastDeviceAddress = prefs.getString(PREF_DEVICE_NAME, null);
|
|
6639
|
-
val
|
|
6640
|
-
(DeviceStore.get("bluetooth", "pending_device_address") as String?)?.takeIf {
|
|
6641
|
-
it.isNotEmpty()
|
|
6642
|
-
}
|
|
6643
|
-
val lastDeviceAddress =
|
|
6644
|
-
pendingAddress
|
|
6645
|
-
?: (DeviceStore.get("bluetooth", "device_address") as String?)
|
|
6675
|
+
val lastDeviceAddress = selectedConnectionAddress()
|
|
6646
6676
|
|
|
6647
6677
|
if (lastDeviceAddress != null && lastDeviceAddress.length > 0) {
|
|
6648
6678
|
// Connect to last known device if available
|
|
@@ -10193,7 +10223,7 @@ class MentraLive : SGCManager() {
|
|
|
10193
10223
|
if (incidentRelay.session == null) {
|
|
10194
10224
|
activeFileTransfers.remove(packetInfo.fileName)
|
|
10195
10225
|
incidentRelay.session =
|
|
10196
|
-
FileTransferSession(packetInfo.fileName, packetInfo.fileSize)
|
|
10226
|
+
FileTransferSession(packetInfo.fileName, packetInfo.fileSize, packetInfo.flags)
|
|
10197
10227
|
incidentRelay.session!!.recalculateTotalPackets(packetInfo.packSize)
|
|
10198
10228
|
Bridge.log(
|
|
10199
10229
|
"LIVE: 📦 Started BLE incident log transfer: " +
|
|
@@ -10244,7 +10274,7 @@ class MentraLive : SGCManager() {
|
|
|
10244
10274
|
// Get or create session for this transfer
|
|
10245
10275
|
if (photoTransfer.session == null) {
|
|
10246
10276
|
photoTransfer.session =
|
|
10247
|
-
FileTransferSession(packetInfo.fileName, packetInfo.fileSize)
|
|
10277
|
+
FileTransferSession(packetInfo.fileName, packetInfo.fileSize, packetInfo.flags)
|
|
10248
10278
|
// Recalculate total packets based on actual pack size (handles variable MTU)
|
|
10249
10279
|
photoTransfer.session!!.recalculateTotalPackets(packetInfo.packSize)
|
|
10250
10280
|
Bridge.log(
|
|
@@ -10330,7 +10360,7 @@ class MentraLive : SGCManager() {
|
|
|
10330
10360
|
var session = activeFileTransfers[packetInfo.fileName]
|
|
10331
10361
|
if (session == null) {
|
|
10332
10362
|
// New file transfer
|
|
10333
|
-
session = FileTransferSession(packetInfo.fileName, packetInfo.fileSize)
|
|
10363
|
+
session = FileTransferSession(packetInfo.fileName, packetInfo.fileSize, packetInfo.flags)
|
|
10334
10364
|
// Recalculate total packets based on actual pack size (handles variable MTU)
|
|
10335
10365
|
session.recalculateTotalPackets(packetInfo.packSize)
|
|
10336
10366
|
activeFileTransfers[packetInfo.fileName] = session
|
|
@@ -10825,6 +10855,9 @@ class MentraLive : SGCManager() {
|
|
|
10825
10855
|
// Send glasses-side loudness / Barrier gate setting.
|
|
10826
10856
|
sendLoudnessGateSetting()
|
|
10827
10857
|
|
|
10858
|
+
// Send glasses-side auto power-off setting.
|
|
10859
|
+
sendAutoPowerOffSetting()
|
|
10860
|
+
|
|
10828
10861
|
// Send mic tuning. With nothing authorized this sends a reset, which is
|
|
10829
10862
|
// what returns a freshly connected pair of glasses to stock behaviour.
|
|
10830
10863
|
sendMicTuningSetting()
|
|
@@ -11108,6 +11141,52 @@ class MentraLive : SGCManager() {
|
|
|
11108
11141
|
}
|
|
11109
11142
|
}
|
|
11110
11143
|
|
|
11144
|
+
override fun sendAutoPowerOffSetting() {
|
|
11145
|
+
val value = DeviceStore.get("bluetooth", "auto_power_off_enabled")
|
|
11146
|
+
val enabled =
|
|
11147
|
+
if (value is Boolean) value
|
|
11148
|
+
else BluetoothSdkDefaults.AUTO_POWER_OFF_ENABLED
|
|
11149
|
+
|
|
11150
|
+
Bridge.log(
|
|
11151
|
+
"LIVE: 🔋 Sending auto power-off setting to glasses: enabled=$enabled" +
|
|
11152
|
+
" (cs_swit type=$AUTO_POWER_OFF_SWITCH_TYPE)"
|
|
11153
|
+
)
|
|
11154
|
+
|
|
11155
|
+
if (!isConnected) {
|
|
11156
|
+
Bridge.log("LIVE: Cannot send auto power-off setting - not connected")
|
|
11157
|
+
return
|
|
11158
|
+
}
|
|
11159
|
+
|
|
11160
|
+
try {
|
|
11161
|
+
val body = JSONObject()
|
|
11162
|
+
body.put("type", AUTO_POWER_OFF_SWITCH_TYPE)
|
|
11163
|
+
body.put("switch", if (enabled) 1 else 0)
|
|
11164
|
+
|
|
11165
|
+
val cmdObject = JSONObject()
|
|
11166
|
+
cmdObject.put("C", "cs_swit")
|
|
11167
|
+
cmdObject.put("V", 1)
|
|
11168
|
+
cmdObject.put("B", body.toString())
|
|
11169
|
+
|
|
11170
|
+
val packedData =
|
|
11171
|
+
K900ProtocolUtils.packDataToK900(
|
|
11172
|
+
cmdObject.toString().toByteArray(StandardCharsets.UTF_8),
|
|
11173
|
+
K900ProtocolUtils.CMD_TYPE_STRING,
|
|
11174
|
+
k900LengthEndian()
|
|
11175
|
+
)
|
|
11176
|
+
if (packedData == null) {
|
|
11177
|
+
Bridge.log("LIVE: Failed to pack auto power-off setting command")
|
|
11178
|
+
return
|
|
11179
|
+
}
|
|
11180
|
+
queueData(packedData)
|
|
11181
|
+
Bridge.log(
|
|
11182
|
+
"LIVE: 🔋 Queued auto power-off cs_swit type=$AUTO_POWER_OFF_SWITCH_TYPE" +
|
|
11183
|
+
" switch=${if (enabled) 1 else 0}"
|
|
11184
|
+
)
|
|
11185
|
+
} catch (e: JSONException) {
|
|
11186
|
+
Log.e(TAG, "Error creating auto power-off setting command", e)
|
|
11187
|
+
}
|
|
11188
|
+
}
|
|
11189
|
+
|
|
11111
11190
|
/** Send button photo settings to glasses, replaying all stored scan-tuning fields. */
|
|
11112
11191
|
override fun sendButtonPhotoSettings() {
|
|
11113
11192
|
val legacyZslMfnr =
|
|
@@ -208,7 +208,7 @@ class MentraNex : SGCManager() {
|
|
|
208
208
|
|
|
209
209
|
// If we already have saved device names for main...
|
|
210
210
|
if (name != null && preferredMainDeviceId != null) {
|
|
211
|
-
if (
|
|
211
|
+
if (name != preferredMainDeviceId) {
|
|
212
212
|
return // Not a matching device
|
|
213
213
|
}
|
|
214
214
|
}
|
|
@@ -230,7 +230,6 @@ class MentraNex : SGCManager() {
|
|
|
230
230
|
type = DeviceTypes.NEX
|
|
231
231
|
hasMic = true
|
|
232
232
|
DeviceStore.apply("glasses", "micEnabled", false)
|
|
233
|
-
preferredMainDeviceId = DeviceManager.getInstance().deviceName
|
|
234
233
|
|
|
235
234
|
// Initialize LC3 audio player
|
|
236
235
|
lc3AudioPlayer = Lc3Player(context)
|
|
@@ -1315,23 +1314,28 @@ class MentraNex : SGCManager() {
|
|
|
1315
1314
|
}
|
|
1316
1315
|
|
|
1317
1316
|
private fun connectToSmartGlasses() {
|
|
1318
|
-
val
|
|
1319
|
-
val deviceAddress =
|
|
1317
|
+
val target = preferredMainDeviceId?.takeIf { it.isNotBlank() } ?: return
|
|
1318
|
+
val deviceAddress = SelectedDeviceAddress.resolve(
|
|
1319
|
+
target,
|
|
1320
|
+
DeviceStore.get("bluetooth", "pending_device_name") as? String,
|
|
1321
|
+
DeviceStore.get("bluetooth", "pending_device_address") as? String,
|
|
1322
|
+
DeviceStore.get("bluetooth", "device_name") as? String,
|
|
1323
|
+
DeviceStore.get("bluetooth", "device_address") as? String,
|
|
1324
|
+
)
|
|
1320
1325
|
|
|
1321
1326
|
// Register bonding receiver
|
|
1322
1327
|
Bridge.log("connectToSmartGlasses start")
|
|
1323
|
-
Bridge.log("
|
|
1324
|
-
preferredMainDeviceId = DeviceManager.getInstance().deviceName
|
|
1328
|
+
Bridge.log("Connecting to selected Nex: $target address: $deviceAddress")
|
|
1325
1329
|
if (!bluetoothAdapter.isEnabled) {
|
|
1326
1330
|
return
|
|
1327
1331
|
}
|
|
1328
1332
|
when {
|
|
1329
|
-
|
|
1333
|
+
deviceAddress != null -> {
|
|
1330
1334
|
stopScan()
|
|
1331
1335
|
mainDevice = bluetoothAdapter.getRemoteDevice(deviceAddress)
|
|
1332
1336
|
mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 0)
|
|
1333
1337
|
}
|
|
1334
|
-
savedNexMainAddress != null -> {
|
|
1338
|
+
savedNexMainName == target && savedNexMainAddress != null -> {
|
|
1335
1339
|
mainDevice = bluetoothAdapter.getRemoteDevice(savedNexMainAddress)
|
|
1336
1340
|
mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 0)
|
|
1337
1341
|
}
|