@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.3.0-beta.942
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 +72 -10
- package/android/build.gradle +15 -0
- package/android/lc3Lib/build.gradle +6 -1
- 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/BluetoothSdkAnalytics.kt +63 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
- 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/ClassicAudioConnectionTracker.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
- 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 +247 -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/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -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/NetworkDiagnosticsTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
- 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/MentraLiveClassicDisconnectTest.kt +133 -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/MentraLiveIncidentDispatchTest.kt +126 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -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 +233 -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 +16 -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 +5 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/nativeLogConsole.d.ts +19 -0
- package/build/_private/nativeLogConsole.d.ts.map +1 -1
- package/build/_private/nativeLogConsole.js +12 -0
- package/build/_private/nativeLogConsole.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +5 -2
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +6 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +6 -6
- 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/build/ota-transport/index.d.ts +4 -0
- package/build/ota-transport/index.d.ts.map +1 -1
- package/build/ota-transport/index.js +2 -0
- package/build/ota-transport/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +17 -4
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +7 -2
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +102 -66
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
- package/ios/Source/MentraBluetoothSDK.swift +313 -54
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/internal/BluetoothAvailability.swift +6 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
- package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
- package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
- package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -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 +272 -197
- 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 +272 -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 +25 -1
- 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/AudioSessionMonitor.swift +44 -8
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G1Text.swift +2 -24
- package/ios/Source/utils/G1TextSanitizer.swift +27 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
- package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
- package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
- package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -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/G1TextSanitizerTests.swift +32 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +217 -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 +194 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/ios/Tests/WifiCredentialLoggingTests.swift +42 -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 +15 -0
- package/src/_private/BluetoothSdkModule.ts +10 -1
- package/src/_private/MentraLocalNetworkModule.ts +28 -0
- package/src/_private/nativeLogConsole.ts +19 -0
- package/src/generated/changelogCatalog.ts +6 -2
- package/src/generated/releaseMetadata.ts +6 -6
- package/src/index.ts +1 -0
- package/src/ota-transport/index.ts +10 -1
|
@@ -8,6 +8,7 @@ import android.content.Context
|
|
|
8
8
|
import android.os.Bundle
|
|
9
9
|
import android.os.Handler
|
|
10
10
|
import android.os.Looper
|
|
11
|
+
import android.util.Log
|
|
11
12
|
import com.mentra.bluetoothsdk.streaming.StreamSessionState
|
|
12
13
|
import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
13
14
|
import com.mentra.bluetoothsdk.utils.PhoneAudioMonitor
|
|
@@ -34,6 +35,21 @@ class MentraBluetoothSdk private constructor(
|
|
|
34
35
|
private val listeners =
|
|
35
36
|
Collections.synchronizedSet(mutableSetOf<MentraBluetoothSdkListener>())
|
|
36
37
|
private val analytics = BluetoothSdkAnalytics(appContext, config.analytics)
|
|
38
|
+
private val updateCheck =
|
|
39
|
+
BluetoothSdkUpdateCheck(appContext, config.analytics.surface) { banner, result ->
|
|
40
|
+
Log.w(UPDATE_LOG_TAG, banner)
|
|
41
|
+
dispatchToListeners {
|
|
42
|
+
it.onRawEvent(
|
|
43
|
+
"sdk_update_notice",
|
|
44
|
+
buildMap {
|
|
45
|
+
put("message", banner)
|
|
46
|
+
put("status", result.status)
|
|
47
|
+
put("installedVersion", BuildConfig.SDK_VERSION)
|
|
48
|
+
result.latestVersion?.let { latest -> put("latestVersion", latest) }
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
37
53
|
private val discoveredDeviceNames = mutableSetOf<String>()
|
|
38
54
|
private val bridgeEventSinkId: String
|
|
39
55
|
private val storeListenerId: String
|
|
@@ -99,6 +115,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
99
115
|
analytics.initializeGlassesStatus(getRawGlassesStatus())
|
|
100
116
|
storeListenerId = DeviceStore.store.addListener { category, changes -> dispatchStoreUpdate(category, changes) }
|
|
101
117
|
analytics.captureStarted()
|
|
118
|
+
updateCheck.start()
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
companion object {
|
|
@@ -106,6 +123,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
106
123
|
private val SCAN_STATE_KEYS = setOf("searching", "searchingController", "searchResults")
|
|
107
124
|
private const val DEFAULT_SCAN_TIMEOUT_MS = 15_000L
|
|
108
125
|
private const val DEFAULT_REQUEST_TIMEOUT_MS = 15_000L
|
|
126
|
+
private const val UPDATE_LOG_TAG = "MentraBluetoothSDK"
|
|
127
|
+
private val MIC_STATE_KEYS = setOf("currentMic", "should_send_transcript", "local_stt_fallback_active")
|
|
109
128
|
// Mirrors AsgConstants: one active + one coalesced pending FOV update, plus BLE margin.
|
|
110
129
|
// Keep in sync with ios/Source/MentraBluetoothSDK.swift and the ASG readiness budget.
|
|
111
130
|
private const val CAMERA_FOV_READY_TIMEOUT_MS = 20_000L
|
|
@@ -240,8 +259,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
240
259
|
// (expo.modules.AsyncFunctionQueue), so a blocked await here froze all
|
|
241
260
|
// miniapp event delivery (Crust.mentraJsDispatchToJs) until the glasses
|
|
242
261
|
// responded. resolve()/reject() stay callable from any thread.
|
|
243
|
-
private class PendingResponse<T>(
|
|
262
|
+
private inner class PendingResponse<T>(
|
|
244
263
|
private val operation: String,
|
|
264
|
+
// The operation without per-request ids, so timeouts group in analytics.
|
|
265
|
+
private val telemetryOperation: String = operation,
|
|
245
266
|
) {
|
|
246
267
|
private val deferred = CompletableDeferred<T>()
|
|
247
268
|
|
|
@@ -258,10 +279,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
258
279
|
// can only mean the deadline elapsed. A rejected deferred rethrows
|
|
259
280
|
// its error from await() and propagates out unchanged.
|
|
260
281
|
return withTimeoutOrNull(timeoutMs) { deferred.await() }
|
|
261
|
-
?:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
282
|
+
?: run {
|
|
283
|
+
analytics.recordUsage { requestTimedOut(telemetryOperation) }
|
|
284
|
+
throw BluetoothSdkException(
|
|
285
|
+
"request_timeout",
|
|
286
|
+
"$operation timed out waiting for glasses response.",
|
|
287
|
+
)
|
|
288
|
+
}
|
|
265
289
|
}
|
|
266
290
|
|
|
267
291
|
}
|
|
@@ -503,6 +527,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
503
527
|
}
|
|
504
528
|
|
|
505
529
|
fun cancelConnectionAttempt() {
|
|
530
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.DISCONNECT_CANCEL)
|
|
506
531
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
507
532
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
508
533
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
|
|
@@ -514,6 +539,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
514
539
|
}
|
|
515
540
|
|
|
516
541
|
fun disconnect() {
|
|
542
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.DISCONNECT_USER)
|
|
517
543
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
518
544
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
519
545
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
|
|
@@ -521,6 +547,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
521
547
|
}
|
|
522
548
|
|
|
523
549
|
fun forget() {
|
|
550
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.DISCONNECT_FORGET)
|
|
524
551
|
deviceManager.forget()
|
|
525
552
|
}
|
|
526
553
|
|
|
@@ -651,6 +678,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
651
678
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "loudness_gate_enabled", enabled)
|
|
652
679
|
}
|
|
653
680
|
|
|
681
|
+
fun setAutoPowerOffEnabled(enabled: Boolean) {
|
|
682
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "auto_power_off_enabled", enabled)
|
|
683
|
+
}
|
|
684
|
+
|
|
654
685
|
@Deprecated(
|
|
655
686
|
message =
|
|
656
687
|
"Sticky action-button photo presets are deprecated. Prefer per-request " +
|
|
@@ -943,8 +974,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
943
974
|
pendingWifiStatus = PendingWifiStatusRequest(ssid, pending)
|
|
944
975
|
}
|
|
945
976
|
try {
|
|
946
|
-
|
|
947
|
-
|
|
977
|
+
return withUsageOutcome(
|
|
978
|
+
{ latencyMs, errorCode, _ -> analytics.recordUsage { wifiSetupSettled(latencyMs, errorCode) } },
|
|
979
|
+
) {
|
|
980
|
+
deviceManager.sendWifiCredentials(ssid, password)
|
|
981
|
+
pending.await()
|
|
982
|
+
}
|
|
948
983
|
} finally {
|
|
949
984
|
synchronized(oneShotLock) {
|
|
950
985
|
if (pendingWifiStatus?.pending === pending) {
|
|
@@ -1058,8 +1093,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
1058
1093
|
pendingHotspotStatus = PendingHotspotStatusRequest(enabled, pending)
|
|
1059
1094
|
}
|
|
1060
1095
|
try {
|
|
1061
|
-
|
|
1062
|
-
|
|
1096
|
+
withUsageOutcome(
|
|
1097
|
+
{ latencyMs, errorCode, _ ->
|
|
1098
|
+
if (enabled) analytics.recordUsage { hotspotStartSettled(latencyMs, errorCode) }
|
|
1099
|
+
},
|
|
1100
|
+
) {
|
|
1101
|
+
deviceManager.setHotspotState(enabled)
|
|
1102
|
+
pending.await()
|
|
1103
|
+
}
|
|
1063
1104
|
} finally {
|
|
1064
1105
|
synchronized(oneShotLock) {
|
|
1065
1106
|
if (pendingHotspotStatus?.pending === pending) {
|
|
@@ -1086,13 +1127,31 @@ class MentraBluetoothSdk private constructor(
|
|
|
1086
1127
|
Bridge.log(
|
|
1087
1128
|
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=${routedRequest.requestId}"
|
|
1088
1129
|
)
|
|
1089
|
-
val pending = PendingResponse<PhotoResponseEvent>("photo request ${routedRequest.requestId}")
|
|
1130
|
+
val pending = PendingResponse<PhotoResponseEvent>("photo request ${routedRequest.requestId}", "photo request")
|
|
1090
1131
|
pendingPhotoRequests[routedRequest.requestId] = pending
|
|
1091
1132
|
try {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1133
|
+
return withUsageOutcome(
|
|
1134
|
+
{ latencyMs, errorCode, event ->
|
|
1135
|
+
val fileSizeBytes = (event?.response as? PhotoResponse.Success)?.fileSizeBytes
|
|
1136
|
+
analytics.recordUsage {
|
|
1137
|
+
photoSettled(
|
|
1138
|
+
size = routedRequest.size.value,
|
|
1139
|
+
mode = routedRequest.mode.value,
|
|
1140
|
+
compress = routedRequest.compress.value,
|
|
1141
|
+
save = routedRequest.save,
|
|
1142
|
+
transferMethod = routedRequest.transferMethod,
|
|
1143
|
+
latencyMs = latencyMs,
|
|
1144
|
+
fileSizeBytes = fileSizeBytes,
|
|
1145
|
+
errorCode = errorCode,
|
|
1146
|
+
)
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
) {
|
|
1150
|
+
deviceManager.requestPhoto(routedRequest)
|
|
1151
|
+
pending.await(
|
|
1152
|
+
if (routedRequest.presendThumbnail) PHOTO_THUMBNAIL_REQUEST_TIMEOUT_MS else PHOTO_REQUEST_TIMEOUT_MS,
|
|
1153
|
+
)
|
|
1154
|
+
}
|
|
1096
1155
|
} finally {
|
|
1097
1156
|
pendingPhotoRequests.remove(routedRequest.requestId, pending)
|
|
1098
1157
|
}
|
|
@@ -1108,7 +1167,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1108
1167
|
mfnr: Boolean? = null,
|
|
1109
1168
|
): CameraStatusEvent {
|
|
1110
1169
|
val effectiveRequestId = nonBlankRequestId(requestId) ?: generatedCameraRequestId("warm")
|
|
1111
|
-
val pending = PendingResponse<CameraStatusEvent>("camera warm up $effectiveRequestId")
|
|
1170
|
+
val pending = PendingResponse<CameraStatusEvent>("camera warm up $effectiveRequestId", "camera warm up")
|
|
1112
1171
|
// Guard against a concurrent warm-up reusing the same requestId: a blind put would overwrite
|
|
1113
1172
|
// (and strand) the first caller's continuation until it times out. Reject the duplicate.
|
|
1114
1173
|
if (pendingCameraStatusRequests.putIfAbsent(effectiveRequestId, pending) != null) {
|
|
@@ -1155,35 +1214,55 @@ class MentraBluetoothSdk private constructor(
|
|
|
1155
1214
|
}
|
|
1156
1215
|
|
|
1157
1216
|
suspend fun startStream(request: StreamRequest): StreamStatusEvent {
|
|
1158
|
-
requireGlassesConnected("start stream")
|
|
1159
|
-
if (!streamSession.supported) {
|
|
1160
|
-
throw BluetoothSdkException("stream_control_unsupported", "Update the glasses software before starting a stream.")
|
|
1161
|
-
}
|
|
1162
1217
|
val message = request.toMap().toMutableMap()
|
|
1163
1218
|
val streamId = (message["streamId"] as? String)?.takeIf { it.isNotBlank() }
|
|
1164
1219
|
?: "sdk-${UUID.randomUUID()}"
|
|
1165
1220
|
message["streamId"] = streamId
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1221
|
+
return withUsageOutcome(
|
|
1222
|
+
{ latencyMs, errorCode, _ ->
|
|
1223
|
+
val glassesNetwork = glassesNetwork()
|
|
1224
|
+
analytics.recordUsage {
|
|
1225
|
+
streamStartSettled(
|
|
1226
|
+
streamUrl = request.streamUrl,
|
|
1227
|
+
width = request.video?.width,
|
|
1228
|
+
height = request.video?.height,
|
|
1229
|
+
fps = request.video?.fps,
|
|
1230
|
+
bitrate = request.video?.bitrate,
|
|
1231
|
+
captureAudio = request.captureAudio,
|
|
1232
|
+
glassesNetwork = glassesNetwork,
|
|
1233
|
+
latencyMs = latencyMs,
|
|
1234
|
+
errorCode = errorCode,
|
|
1235
|
+
)
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
) {
|
|
1239
|
+
requireGlassesConnected("start stream")
|
|
1240
|
+
if (!streamSession.supported) {
|
|
1241
|
+
throw BluetoothSdkException("stream_control_unsupported", "Update the glasses software before starting a stream.")
|
|
1242
|
+
}
|
|
1243
|
+
val pending = PendingResponse<StreamStatusEvent>("start stream $streamId", "start stream")
|
|
1244
|
+
val requestedAt = System.currentTimeMillis()
|
|
1245
|
+
analytics.recordUsage { streamRequested(streamId, request.streamUrl, requestedAt) }
|
|
1246
|
+
var start: PendingStreamStart? = null
|
|
1247
|
+
try {
|
|
1248
|
+
// seq assignment and the BLE hand-off must be one critical section:
|
|
1249
|
+
// rejectPreemptedStreamStarts only works if seq order equals the
|
|
1250
|
+
// order the commands reach the glasses' FIFO.
|
|
1251
|
+
synchronized(streamStartOrderLock) {
|
|
1252
|
+
val registered = PendingStreamStart(streamStartSeq.incrementAndGet(), pending)
|
|
1253
|
+
pendingStreamStarts[streamId] = registered
|
|
1254
|
+
start = registered
|
|
1255
|
+
deviceManager.startStream(message)
|
|
1256
|
+
}
|
|
1257
|
+
pending.await(STREAM_START_TIMEOUT_MS)
|
|
1258
|
+
} finally {
|
|
1259
|
+
start?.let { pendingStreamStarts.remove(streamId, it) }
|
|
1177
1260
|
}
|
|
1178
|
-
val event = pending.await(STREAM_START_TIMEOUT_MS)
|
|
1179
|
-
return event
|
|
1180
|
-
} finally {
|
|
1181
|
-
start?.let { pendingStreamStarts.remove(streamId, it) }
|
|
1182
1261
|
}
|
|
1183
1262
|
}
|
|
1184
1263
|
|
|
1185
1264
|
suspend fun rgbLedControl(request: RgbLedRequest): RgbLedControlResponseEvent {
|
|
1186
|
-
val pending = PendingResponse<RgbLedControlResponseEvent>("RGB LED command ${request.requestId}")
|
|
1265
|
+
val pending = PendingResponse<RgbLedControlResponseEvent>("RGB LED command ${request.requestId}", "RGB LED command")
|
|
1187
1266
|
pendingRgbLedRequests[request.requestId] = pending
|
|
1188
1267
|
try {
|
|
1189
1268
|
deviceManager.rgbLedControl(
|
|
@@ -1202,6 +1281,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1202
1281
|
}
|
|
1203
1282
|
|
|
1204
1283
|
suspend fun stopStream(): StreamStatusEvent {
|
|
1284
|
+
analytics.updateUsage { streamStopRequested() }
|
|
1205
1285
|
val pending = PendingResponse<StreamStatusEvent>("stop stream")
|
|
1206
1286
|
val targetStreamId = streamSession.currentStreamId
|
|
1207
1287
|
try {
|
|
@@ -1243,6 +1323,16 @@ class MentraBluetoothSdk private constructor(
|
|
|
1243
1323
|
"A video recording command is already waiting for requestId ${request.requestId}.",
|
|
1244
1324
|
)
|
|
1245
1325
|
}
|
|
1326
|
+
analytics.updateUsage {
|
|
1327
|
+
videoRequested(
|
|
1328
|
+
request.requestId,
|
|
1329
|
+
request.width,
|
|
1330
|
+
request.height,
|
|
1331
|
+
request.fps,
|
|
1332
|
+
request.save,
|
|
1333
|
+
request.maxRecordingTimeMinutes,
|
|
1334
|
+
)
|
|
1335
|
+
}
|
|
1246
1336
|
try {
|
|
1247
1337
|
deviceManager.startVideoRecording(
|
|
1248
1338
|
request.requestId,
|
|
@@ -1281,6 +1371,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1281
1371
|
"A video recording command is already waiting for requestId $requestId.",
|
|
1282
1372
|
)
|
|
1283
1373
|
}
|
|
1374
|
+
analytics.updateUsage { videoStopRequested(requestId) }
|
|
1284
1375
|
try {
|
|
1285
1376
|
deviceManager.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1286
1377
|
val timeoutMs =
|
|
@@ -1587,6 +1678,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1587
1678
|
Bridge.removeEventSink(bridgeEventSinkId)
|
|
1588
1679
|
DeviceStore.store.removeListener(storeListenerId)
|
|
1589
1680
|
analytics.shutdown()
|
|
1681
|
+
updateCheck.stop()
|
|
1590
1682
|
listeners.clear()
|
|
1591
1683
|
}
|
|
1592
1684
|
|
|
@@ -1596,7 +1688,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1596
1688
|
if (changes["connected"] == false) {
|
|
1597
1689
|
resetWifiProtocolSession("", "wifi_session_disconnected")
|
|
1598
1690
|
}
|
|
1599
|
-
analytics.observeGlassesStatus(getRawGlassesStatus())
|
|
1691
|
+
analytics.observeGlassesStatus(getRawGlassesStatus(), ::isPhoneBluetoothOff)
|
|
1600
1692
|
val state = getState()
|
|
1601
1693
|
dispatchToListeners {
|
|
1602
1694
|
it.onStateChanged(state)
|
|
@@ -1604,6 +1696,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
1604
1696
|
}
|
|
1605
1697
|
}
|
|
1606
1698
|
ObservableStore.BLUETOOTH_CATEGORY -> {
|
|
1699
|
+
if (analytics.isEnabled && changes.keys.any { it in MIC_STATE_KEYS }) {
|
|
1700
|
+
val bluetooth = getRawBluetoothStatus()
|
|
1701
|
+
val transcribing = bluetooth.shouldSendTranscript || bluetooth.localSttFallbackActive
|
|
1702
|
+
val now = System.currentTimeMillis()
|
|
1703
|
+
analytics.recordUsage { observeMic(bluetooth.currentMic, transcribing, now) }
|
|
1704
|
+
}
|
|
1607
1705
|
val state = getState()
|
|
1608
1706
|
val scanChanged = changes.keys.any { it in SCAN_STATE_KEYS }
|
|
1609
1707
|
dispatchToListeners {
|
|
@@ -1870,6 +1968,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
1870
1968
|
"photo_response" -> {
|
|
1871
1969
|
val event = PhotoResponseEvent(data)
|
|
1872
1970
|
handlePhotoResponseForRequests(event)
|
|
1971
|
+
val response = event.response
|
|
1972
|
+
if (response is PhotoResponse.Error && event.requestId == BluetoothSdkUsageTracker.LOCAL_PHOTO_REQUEST_ID) {
|
|
1973
|
+
analytics.recordUsage { buttonPhotoFailed(response.errorCode) }
|
|
1974
|
+
}
|
|
1873
1975
|
dispatchToListeners { it.onPhotoResponse(event) }
|
|
1874
1976
|
}
|
|
1875
1977
|
"photo_status" -> dispatchToListeners { it.onPhotoStatus(PhotoStatusEvent(data)) }
|
|
@@ -1881,6 +1983,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
1881
1983
|
"video_recording_status" -> {
|
|
1882
1984
|
val event = VideoRecordingStatusEvent(data)
|
|
1883
1985
|
handleVideoRecordingStatusForRequests(event)
|
|
1986
|
+
val now = System.currentTimeMillis()
|
|
1987
|
+
analytics.recordUsage { videoStatus(event.requestId, event.status, event.success, now) }
|
|
1884
1988
|
dispatchToListeners { it.onVideoRecordingStatus(event) }
|
|
1885
1989
|
}
|
|
1886
1990
|
"media_success", "media_error" -> {
|
|
@@ -1900,6 +2004,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1900
2004
|
if (data.containsKey("revision") && !streamSession.accept(data)) return
|
|
1901
2005
|
val event = StreamStatusEvent(data)
|
|
1902
2006
|
handleStreamStatusForRequests(event)
|
|
2007
|
+
recordStreamStatus(event)
|
|
1903
2008
|
dispatchToListeners { it.onStreamStatus(event) }
|
|
1904
2009
|
}
|
|
1905
2010
|
"keep_alive_ack" -> {
|
|
@@ -1924,6 +2029,23 @@ class MentraBluetoothSdk private constructor(
|
|
|
1924
2029
|
)
|
|
1925
2030
|
}
|
|
1926
2031
|
}
|
|
2032
|
+
if (analytics.isEnabled) {
|
|
2033
|
+
val glasses = getRawGlassesStatus()
|
|
2034
|
+
val now = System.currentTimeMillis()
|
|
2035
|
+
analytics.recordUsage {
|
|
2036
|
+
otaStatus(
|
|
2037
|
+
sessionId = event.sessionId,
|
|
2038
|
+
status = event.status,
|
|
2039
|
+
stepType = event.stepType,
|
|
2040
|
+
totalSteps = event.totalSteps,
|
|
2041
|
+
errorMessage = event.errorMessage,
|
|
2042
|
+
fromBuildNumber = glasses.buildNumber,
|
|
2043
|
+
fromBesFirmwareVersion = glasses.besFirmwareVersion,
|
|
2044
|
+
fromMtkFirmwareVersion = glasses.mtkFirmwareVersion,
|
|
2045
|
+
nowMs = now,
|
|
2046
|
+
)
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
1927
2049
|
dispatchToListeners { it.onOtaStatus(event) }
|
|
1928
2050
|
}
|
|
1929
2051
|
"mic_health" -> {
|
|
@@ -1979,6 +2101,75 @@ class MentraBluetoothSdk private constructor(
|
|
|
1979
2101
|
}
|
|
1980
2102
|
}
|
|
1981
2103
|
|
|
2104
|
+
private fun recordStreamStatus(event: StreamStatusEvent) {
|
|
2105
|
+
if (!analytics.isEnabled) return
|
|
2106
|
+
val status = event.status
|
|
2107
|
+
val attempt =
|
|
2108
|
+
when (status) {
|
|
2109
|
+
is StreamStatus.Reconnecting -> status.attempt
|
|
2110
|
+
is StreamStatus.Reconnected -> status.attempt
|
|
2111
|
+
else -> null
|
|
2112
|
+
}
|
|
2113
|
+
val errorDetails = event.errorDetails ?: (status as? StreamStatus.Error)?.errorDetails
|
|
2114
|
+
val now = System.currentTimeMillis()
|
|
2115
|
+
analytics.recordUsage {
|
|
2116
|
+
streamStatus(
|
|
2117
|
+
streamId = event.streamId,
|
|
2118
|
+
state = event.state.value,
|
|
2119
|
+
terminal = event.terminal,
|
|
2120
|
+
reconnectAttempt = attempt,
|
|
2121
|
+
bitrate = event.stats?.bitrate,
|
|
2122
|
+
fps = event.stats?.fps,
|
|
2123
|
+
droppedFrames = event.stats?.droppedFrames,
|
|
2124
|
+
temperatureC = event.stats?.temperatureC,
|
|
2125
|
+
errorDetails = errorDetails,
|
|
2126
|
+
nowMs = now,
|
|
2127
|
+
)
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
private fun glassesNetwork(): String {
|
|
2132
|
+
val glasses = getRawGlassesStatus()
|
|
2133
|
+
return when {
|
|
2134
|
+
glasses.hotspot is HotspotStatus.Enabled -> "hotspot"
|
|
2135
|
+
glasses.wifi is WifiStatus.Connected -> "wifi"
|
|
2136
|
+
else -> "none"
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
private fun isPhoneBluetoothOff(): Boolean =
|
|
2141
|
+
try {
|
|
2142
|
+
(appContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter?.isEnabled == false
|
|
2143
|
+
} catch (_: Exception) {
|
|
2144
|
+
false
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* Runs a glasses command and reports its latency and outcome to usage
|
|
2149
|
+
* analytics. Cancellation is the caller giving up, not a command outcome, so
|
|
2150
|
+
* it is not reported.
|
|
2151
|
+
*/
|
|
2152
|
+
private inline fun <T> withUsageOutcome(
|
|
2153
|
+
record: (latencyMs: Long, errorCode: String?, result: T?) -> Unit,
|
|
2154
|
+
block: () -> T,
|
|
2155
|
+
): T {
|
|
2156
|
+
val startedAt = System.currentTimeMillis()
|
|
2157
|
+
val result =
|
|
2158
|
+
try {
|
|
2159
|
+
block()
|
|
2160
|
+
} catch (error: CancellationException) {
|
|
2161
|
+
throw error
|
|
2162
|
+
} catch (error: Exception) {
|
|
2163
|
+
if (analytics.isEnabled) {
|
|
2164
|
+
val errorCode = (error as? BluetoothSdkException)?.code ?: error.javaClass.simpleName
|
|
2165
|
+
record(System.currentTimeMillis() - startedAt, errorCode, null)
|
|
2166
|
+
}
|
|
2167
|
+
throw error
|
|
2168
|
+
}
|
|
2169
|
+
if (analytics.isEnabled) record(System.currentTimeMillis() - startedAt, null, result)
|
|
2170
|
+
return result
|
|
2171
|
+
}
|
|
2172
|
+
|
|
1982
2173
|
private fun dispatchToListeners(callback: (MentraBluetoothSdkListener) -> Unit) {
|
|
1983
2174
|
val snapshot = synchronized(listeners) { listeners.toList() }
|
|
1984
2175
|
val deliver = {
|
|
@@ -41,6 +41,30 @@ class MentraLocalNetworkModule : Module() {
|
|
|
41
41
|
Name("MentraLocalNetwork")
|
|
42
42
|
Events("downloadProgress", "networkLost")
|
|
43
43
|
|
|
44
|
+
Function("getNetworkDiagnostics") {
|
|
45
|
+
val manager = connectivityManager()
|
|
46
|
+
// Sample time marks the beginning of these sequential local OS reads.
|
|
47
|
+
val sampledAtMs = System.currentTimeMillis()
|
|
48
|
+
networkDiagnostics(
|
|
49
|
+
sampledAtMs,
|
|
50
|
+
manager.boundNetworkForProcess,
|
|
51
|
+
manager.activeNetwork,
|
|
52
|
+
synchronized(lock) { localNetwork },
|
|
53
|
+
) { network ->
|
|
54
|
+
manager.getNetworkCapabilities(network)?.let { capabilities ->
|
|
55
|
+
mapOf(
|
|
56
|
+
"wifi" to capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI),
|
|
57
|
+
"cellular" to capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR),
|
|
58
|
+
"ethernet" to capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET),
|
|
59
|
+
"vpn" to capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN),
|
|
60
|
+
"internet" to capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET),
|
|
61
|
+
"validated" to capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED),
|
|
62
|
+
"captivePortal" to capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
AsyncFunction("connect") { ssid: String, password: String, promise: Promise ->
|
|
45
69
|
connect(ssid, password, promise)
|
|
46
70
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
/** Only routing relationships and capabilities cross the bridge, never network identities. */
|
|
4
|
+
internal fun <N> networkDiagnostics(
|
|
5
|
+
sampledAtMs: Long,
|
|
6
|
+
processBound: N?,
|
|
7
|
+
activeNetwork: N?,
|
|
8
|
+
glassesLocal: N?,
|
|
9
|
+
capabilities: (N) -> Map<String, Boolean>?,
|
|
10
|
+
): Map<String, Any?> {
|
|
11
|
+
fun observation(network: N?): Map<String, Any?> = mapOf(
|
|
12
|
+
"present" to (network != null),
|
|
13
|
+
"capabilities" to network?.let(capabilities),
|
|
14
|
+
)
|
|
15
|
+
fun same(first: N?, second: N?): Boolean = first != null && first == second
|
|
16
|
+
return mapOf(
|
|
17
|
+
"sampledAtMs" to sampledAtMs,
|
|
18
|
+
"processBound" to observation(processBound),
|
|
19
|
+
"activeNetwork" to observation(activeNetwork),
|
|
20
|
+
"glassesLocal" to observation(glassesLocal),
|
|
21
|
+
"processBoundMatchesActive" to same(processBound, activeNetwork),
|
|
22
|
+
"processBoundMatchesGlassesLocal" to same(processBound, glassesLocal),
|
|
23
|
+
"activeNetworkMatchesGlassesLocal" to same(activeNetwork, glassesLocal),
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -6,13 +6,14 @@ internal sealed interface VersionInfoAccumulatorOutcome {
|
|
|
6
6
|
data class Complete(val result: VersionInfoResult) : VersionInfoAccumulatorOutcome
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/** One request, explicit modern completion, or the
|
|
9
|
+
/** One request, explicit modern completion, or the legacy firmware's terminal chunk. */
|
|
10
10
|
internal class VersionInfoResponseAccumulator(private val expectedRequestId: String) {
|
|
11
11
|
private val values = mutableMapOf<String, Any>()
|
|
12
12
|
private val chunks = mutableMapOf<Int, Map<String, Any>>()
|
|
13
13
|
private var count: Int? = null
|
|
14
14
|
private var sid: String? = null
|
|
15
15
|
private var legacyStarted = false
|
|
16
|
+
private var legacyTwoChunkResponse = false
|
|
16
17
|
private var completed = false
|
|
17
18
|
|
|
18
19
|
fun accept(event: Map<String, Any>): VersionInfoAccumulatorOutcome {
|
|
@@ -41,10 +42,20 @@ internal class VersionInfoResponseAccumulator(private val expectedRequestId: Str
|
|
|
41
42
|
if (count != null) return VersionInfoAccumulatorOutcome.Ignored
|
|
42
43
|
when (event[RESPONSE_CHUNK_KEY] as? String ?: "version_info") {
|
|
43
44
|
"version_info" -> { values.clear(); merge(event); return finish() }
|
|
44
|
-
"version_info_1" -> {
|
|
45
|
+
"version_info_1" -> {
|
|
46
|
+
values.clear()
|
|
47
|
+
legacyStarted = true
|
|
48
|
+
legacyTwoChunkResponse = event["buildNumber"] == "27"
|
|
49
|
+
merge(event)
|
|
50
|
+
}
|
|
45
51
|
"version_info_2" -> {
|
|
46
52
|
if (!legacyStarted) return VersionInfoAccumulatorOutcome.Ignored
|
|
53
|
+
// Factory ASG27 sends only chunks 1 and 2. Its second chunk carries the
|
|
54
|
+
// OTA URL and legacy firmware version; ASG31+ moves firmware to chunk 3.
|
|
47
55
|
merge(event)
|
|
56
|
+
if (legacyTwoChunkResponse && values["buildNumber"] == "27" &&
|
|
57
|
+
!(event["otaVersionUrl"] as? String).isNullOrBlank()
|
|
58
|
+
) return finish()
|
|
48
59
|
}
|
|
49
60
|
"version_info_3" -> {
|
|
50
61
|
if (!legacyStarted) return VersionInfoAccumulatorOutcome.Ignored
|
|
@@ -108,6 +108,9 @@ object BleTraceLogger {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
private fun sanitizeValue(key: String?, value: Any?): Any? {
|
|
111
|
+
// Scan security metadata is a boolean, not a credential. Keep the exact
|
|
112
|
+
// typed field while still redacting string/unknown values under this key.
|
|
113
|
+
if (key == "requiresPassword" && value is Boolean) return value
|
|
111
114
|
if (key != null && sensitiveKeyParts.any { key.contains(it, ignoreCase = true) }) {
|
|
112
115
|
return "<redacted>"
|
|
113
116
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk
|
|
2
2
|
|
|
3
3
|
internal object DashboardContentFormatter {
|
|
4
|
-
const val STATUS_HEADER = "\$TIME12$ \$DATE$ \$GBATT$
|
|
4
|
+
const val STATUS_HEADER = "\$TIME12$ \$DATE$ \$GBATT$"
|
|
5
5
|
|
|
6
6
|
fun template(content: String): String =
|
|
7
7
|
if (content.isEmpty()) STATUS_HEADER else "$STATUS_HEADER\n\n$content"
|
|
@@ -13,6 +13,8 @@ import android.os.IBinder
|
|
|
13
13
|
import androidx.core.app.NotificationCompat
|
|
14
14
|
import androidx.core.content.ContextCompat
|
|
15
15
|
import com.mentra.bluetoothsdk.Bridge
|
|
16
|
+
import com.mentra.bluetoothsdk.DeviceManager
|
|
17
|
+
import com.mentra.bluetoothsdk.DeviceStore
|
|
16
18
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
17
19
|
|
|
18
20
|
class ForegroundService : Service() {
|
|
@@ -107,9 +109,40 @@ class ForegroundService : Service() {
|
|
|
107
109
|
}
|
|
108
110
|
// Re-check permissions in case they changed
|
|
109
111
|
startForegroundWithAutoDetectedType()
|
|
112
|
+
// A null intent is Android restarting START_STICKY after process death.
|
|
113
|
+
// Do not wait for an Activity/Expo module to restore the native BLE owner.
|
|
114
|
+
if (intent == null && !DeviceManager.isInitialized()) {
|
|
115
|
+
val settings = G2ConnectionRecovery(this).read()
|
|
116
|
+
if (settings != null && !startHostRuntimeRecovery()) {
|
|
117
|
+
Bridge.initialize(applicationContext)
|
|
118
|
+
settings.forEach { (key, value) -> DeviceStore.set("bluetooth", key, value) }
|
|
119
|
+
Bridge.log("ForegroundService: restoring G2 connection after process restart")
|
|
120
|
+
try {
|
|
121
|
+
DeviceManager.getInstance().connectDefault()
|
|
122
|
+
} catch (error: Exception) {
|
|
123
|
+
Bridge.log("ForegroundService: G2 recovery failed: ${error.message}")
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
110
127
|
return START_STICKY
|
|
111
128
|
}
|
|
112
129
|
|
|
130
|
+
private fun startHostRuntimeRecovery(): Boolean {
|
|
131
|
+
// Keep the standalone SDK free of React dependencies. The Mentra App
|
|
132
|
+
// opts in with a private headless service declared by its host module.
|
|
133
|
+
val serviceName = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
|
|
134
|
+
.metaData?.getString("com.mentra.bluetoothsdk.RUNTIME_RECOVERY_SERVICE")
|
|
135
|
+
?: return false
|
|
136
|
+
return try {
|
|
137
|
+
if (startService(Intent().setClassName(packageName, serviceName)) == null) return false
|
|
138
|
+
Bridge.log("ForegroundService: requested host JavaScript runtime recovery")
|
|
139
|
+
true
|
|
140
|
+
} catch (error: Exception) {
|
|
141
|
+
Bridge.log("ForegroundService: host runtime recovery could not start: ${error.message}")
|
|
142
|
+
false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
113
146
|
private fun startForegroundWithAutoDetectedType() {
|
|
114
147
|
startForegroundWithType(detectServiceType())
|
|
115
148
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.services
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
|
|
7
|
+
/** Native recovery of the last active G2 link when Android restarts our sticky service. */
|
|
8
|
+
internal class G2ConnectionRecovery(context: Context) {
|
|
9
|
+
private val preferences = context.getSharedPreferences("g2_connection_recovery", Context.MODE_PRIVATE)
|
|
10
|
+
|
|
11
|
+
companion object {
|
|
12
|
+
// Deliberately exclude transient connection flags, mic intent, tokens and pending pairing.
|
|
13
|
+
internal val keys = setOf(
|
|
14
|
+
"default_wearable", "device_name", "device_address", "project_name",
|
|
15
|
+
"brightness", "auto_brightness", "dashboard_height", "dashboard_depth",
|
|
16
|
+
"head_up_angle", "use_native_dashboard", "twelve_hour_time", "metric_system",
|
|
17
|
+
"screen_disabled", "power_saving_mode", "lc3_frame_size",
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun save(settings: Map<String, Any>) {
|
|
22
|
+
if (settings["default_wearable"] != DeviceTypes.G2 ||
|
|
23
|
+
(settings["device_name"] as? String).isNullOrBlank()) {
|
|
24
|
+
clear()
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
val snapshot = settings.filterKeys { it in keys }
|
|
28
|
+
preferences.edit().putString("snapshot", JSONObject(snapshot).toString()).apply()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun clear() {
|
|
32
|
+
preferences.edit().remove("snapshot").apply()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fun read(): Map<String, Any>? {
|
|
36
|
+
val raw = preferences.getString("snapshot", null) ?: return null
|
|
37
|
+
return try {
|
|
38
|
+
val json = JSONObject(raw)
|
|
39
|
+
if (json.optString("default_wearable") != DeviceTypes.G2 ||
|
|
40
|
+
json.optString("device_name").isBlank()) return null
|
|
41
|
+
keys.filter { json.has(it) && !json.isNull(it) }.associateWith { json.get(it) }
|
|
42
|
+
} catch (_: Exception) {
|
|
43
|
+
clear()
|
|
44
|
+
null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|