@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
package/README.md
CHANGED
|
@@ -217,9 +217,9 @@ reordering rows just because RSSI metadata arrives later.
|
|
|
217
217
|
|
|
218
218
|
## Mentra SDK Usage Analytics
|
|
219
219
|
|
|
220
|
-
The SDK sends
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
The SDK sends usage events to Mentra's PostHog project by default so Mentra
|
|
221
|
+
can understand SDK adoption, glasses connections, enterprise device
|
|
222
|
+
deployments, and how Mentra Live features are used:
|
|
223
223
|
|
|
224
224
|
- `bluetooth_sdk_started`: sent once per app runtime after the native SDK starts.
|
|
225
225
|
- `bluetooth_sdk_glasses_connected`: sent when SDK status transitions from not connected to connected.
|
|
@@ -229,6 +229,45 @@ The connected event waits for the glasses model when the model is not known at
|
|
|
229
229
|
the moment the connection flag flips; if the connection ends first it is sent
|
|
230
230
|
without a model and with `glasses_model_unresolved=true`.
|
|
231
231
|
|
|
232
|
+
Each usage occurrence below is one event, sent when it happens:
|
|
233
|
+
|
|
234
|
+
- `bluetooth_sdk_glasses_disconnected`: a connection reported as connected
|
|
235
|
+
ended. Carries `session_duration_ms`, `disconnect_reason` (`user_disconnect`,
|
|
236
|
+
`forget`, `cancel`, `bluetooth_off`, `link_lost`, or `unknown`), and the
|
|
237
|
+
battery levels seen during the connection. A connection already up when the
|
|
238
|
+
SDK started is not reported.
|
|
239
|
+
- `bluetooth_sdk_photo_taken`: a `requestPhoto` call settled, or a photo taken
|
|
240
|
+
with the glasses button failed (successful button photos never reach the
|
|
241
|
+
phone). Carries `trigger` (`api` / `button`), `success`, `error_code`,
|
|
242
|
+
`latency_ms`, the requested size, mode, compression, gallery save and
|
|
243
|
+
transfer method, and `file_size_bytes` when the response carries it.
|
|
244
|
+
- `bluetooth_sdk_video_recorded`: an app- or button-started recording stopped
|
|
245
|
+
or failed. Carries `trigger`, `success`, `error_code`, `duration_ms` (phone
|
|
246
|
+
clock, start to stop status), the requested resolution and frame rate,
|
|
247
|
+
gallery save, maximum duration, and `stopped_by_app`.
|
|
248
|
+
- `bluetooth_sdk_stream_started` / `bluetooth_sdk_stream_ended`: a
|
|
249
|
+
`startStream` call settled, and that stream later ended. `protocol` comes
|
|
250
|
+
from the URL scheme only (`rtmp`, `srt`, `whip`, or `unknown`). The start
|
|
251
|
+
carries the requested video settings, `capture_audio`, `glasses_network`, and
|
|
252
|
+
`latency_ms`; the end carries `duration_ms`, `end_state`, `stopped_by_app`,
|
|
253
|
+
`reconnect_attempts`, `error_code`, and the last bitrate, frame rate, dropped
|
|
254
|
+
frames, and temperature when the stream reported statistics.
|
|
255
|
+
- `bluetooth_sdk_mic_session`: the active microphone turned off or changed.
|
|
256
|
+
Carries `duration_ms`, `mic_source`, and `local_transcription`.
|
|
257
|
+
- `bluetooth_sdk_wifi_setup` / `bluetooth_sdk_hotspot_started`:
|
|
258
|
+
`sendWifiCredentials` or `setHotspotState(true)` settled, with `success`,
|
|
259
|
+
`error_code`, and `latency_ms`.
|
|
260
|
+
- `bluetooth_sdk_ota_finished`: a glasses software update completed or failed,
|
|
261
|
+
with the failed step and error, `total_steps`, `duration_ms`, and the
|
|
262
|
+
glasses versions it started from.
|
|
263
|
+
- `bluetooth_sdk_request_failed`: a glasses command timed out
|
|
264
|
+
(`error_code=request_timeout`), named by `operation`. At most 100 per SDK
|
|
265
|
+
start.
|
|
266
|
+
|
|
267
|
+
These usage events also carry the connected glasses' `glasses_model`,
|
|
268
|
+
`glasses_device_id` and `glasses_build_number` when known. A recording or
|
|
269
|
+
stream whose end never reaches the running app produces only its start.
|
|
270
|
+
|
|
232
271
|
Analytics delivery never blocks Bluetooth SDK behavior: events are submitted
|
|
233
272
|
asynchronously off the caller thread. An upload that fails (no network, non-2xx)
|
|
234
273
|
is kept in a small on-device queue (at most 100 events, 7 days) and retried on
|
|
@@ -281,10 +320,30 @@ write token, not a private PostHog personal API key. Apps do not configure the
|
|
|
281
320
|
analytics destination; these SDK usage events are always sent to Mentra's
|
|
282
321
|
PostHog project unless analytics are disabled.
|
|
283
322
|
|
|
323
|
+
### Update check
|
|
324
|
+
|
|
325
|
+
Separately from analytics, the SDK checks for a newer SDK release every time it
|
|
326
|
+
starts, whether or not analytics are disabled. The check is one HTTPS request
|
|
327
|
+
(4-second timeout, no retry) to
|
|
328
|
+
`https://core.mentraglass.com/api/client/bluetooth-sdk/update-check` carrying
|
|
329
|
+
`install_id` (the same anonymous install id analytics uses), `app_identifier`,
|
|
330
|
+
`app_version`, `app_build`, `app_build_type`, `app_install_source`,
|
|
331
|
+
`app_environment`, `sdk_version`, `sdk_platform`, `sdk_surface`,
|
|
332
|
+
`os_platform`, and `os_version`. Mentra records each check. The SDK compares
|
|
333
|
+
itself with the latest release on its own channel (stable, beta, or dev).
|
|
334
|
+
|
|
335
|
+
When a newer release exists, the SDK prints a banner with the update command to
|
|
336
|
+
the developer console: Logcat (`MentraBluetoothSDK` tag) on Android, the Xcode
|
|
337
|
+
console on iOS, and `console.warn` in React Native, which shows as a LogBox
|
|
338
|
+
warning in debug builds. The banner repeats every 15 minutes while the SDK
|
|
339
|
+
instance lives. Nothing is shown in app UI. A network that blocks
|
|
340
|
+
`core.mentraglass.com` loses only the banner; SDK behavior is unchanged.
|
|
341
|
+
|
|
284
342
|
Captured properties include `event_source`, `sdk_platform`, `sdk_surface`,
|
|
285
343
|
`sdk_version`, `app_identifier` (the Android package or iOS bundle identifier),
|
|
286
344
|
the platform-specific `app_package` or `app_bundle_identifier`, OS
|
|
287
|
-
platform/version,
|
|
345
|
+
platform/version, `phone_manufacturer`, `phone_model`, `locale`, `timezone`,
|
|
346
|
+
and `event_kind`. Every event also carries host build facts:
|
|
288
347
|
`app_version`, `app_build`, `app_build_type` (`debug` / `release`),
|
|
289
348
|
`app_install_source` (`play_store`, `app_store`, `testflight`, `adhoc_or_dev`,
|
|
290
349
|
`simulator`, `sideload`, a named third-party store, `other_store`, or `unknown`
|
|
@@ -308,8 +367,11 @@ decode it from the glasses' BLE advertisement / manufacturer data.
|
|
|
308
367
|
|
|
309
368
|
Apart from that glasses serial, the SDK does not upload BLE MAC addresses,
|
|
310
369
|
CoreBluetooth identifiers, the host phone's Android device serial, Bluetooth
|
|
311
|
-
device names, user ids, tokens, Wi-Fi
|
|
312
|
-
|
|
370
|
+
device names, user ids, tokens, Wi-Fi names or credentials, stream, webhook or
|
|
371
|
+
upload URLs, microphone data, photos, videos, or transcripts, and it never reads
|
|
372
|
+
device location. Error details have URLs removed before they are sent. PostHog
|
|
373
|
+
derives an approximate location (country, region, city) from the IP address
|
|
374
|
+
each event is sent from. PostHog receives a locally generated anonymous SDK install id as
|
|
313
375
|
`distinct_id`, and events include `$process_person_profile: false`.
|
|
314
376
|
|
|
315
377
|
## React Hooks
|
|
@@ -446,9 +508,9 @@ console.log(ledAck.state)
|
|
|
446
508
|
|
|
447
509
|
## Dashboard Content
|
|
448
510
|
|
|
449
|
-
`setDashboardContent(content)` keeps the standard `$TIME12$ $DATE$ $GBATT
|
|
450
|
-
|
|
451
|
-
|
|
511
|
+
`setDashboardContent(content)` keeps the standard `$TIME12$ $DATE$ $GBATT$`
|
|
512
|
+
status header and places non-empty content below it after a blank line. Pass the
|
|
513
|
+
exact empty string to reset the dashboard to the status
|
|
452
514
|
header only. The template is held in memory for the SDK process/session, and its
|
|
453
515
|
status placeholders are refreshed each time the dashboard renders. Calling this
|
|
454
516
|
method does not open the dashboard; it updates an active contextual dashboard
|
|
@@ -507,7 +569,7 @@ Disabling restores normal hotspot-only behavior: the server rebinds to an active
|
|
|
507
569
|
Both the native SDK and ASG client need this command; older glasses firmware times out without claiming success. The client app must have local-network access and permit cleartext HTTP to the local device under its platform/MDM policy.
|
|
508
570
|
|
|
509
571
|
WiFi, hotspot, and version-info commands resolve from the ASG response path, not local dispatch:
|
|
510
|
-
`requestWifiScan()` resolves from the ASG `wifi_scan_result` completion response with the updated scan list, including `[]` when no networks are found. Intermediate `wifi_scan_result` events can arrive with `scanComplete: false` while the glasses stream discovered networks; the final event uses `scanComplete: true`. If older glasses stream non-empty scan results but never send the completion event, the request resolves with the accumulated scan list when the request times out. `sendWifiCredentials()` resolves when the requested SSID is connected, `forgetWifiNetwork()` returns a semantic `WifiForgetResult`, `getSavedWifiNetworks()` returns a semantic `SavedWifiNetworksResult`, `setHotspotState()` resolves when the requested hotspot state is reported, and `requestVersionInfo()` waits for all chunks declared by the correlated response's `chunkCount`, `chunkIndex`, and `final` metadata. Legacy single-message responses complete immediately; legacy chunked responses complete on `version_info_3` after `version_info_1`. Missing final chunks time out rather than returning partial data after a quiet period.
|
|
572
|
+
`requestWifiScan()` resolves from the ASG `wifi_scan_result` completion response with the updated scan list, including `[]` when no networks are found. Intermediate `wifi_scan_result` events can arrive with `scanComplete: false` while the glasses stream discovered networks; the final event uses `scanComplete: true`. If older glasses stream non-empty scan results but never send the completion event, the request resolves with the accumulated scan list when the request times out. `sendWifiCredentials()` resolves when the requested SSID is connected, `forgetWifiNetwork()` returns a semantic `WifiForgetResult`, `getSavedWifiNetworks()` returns a semantic `SavedWifiNetworksResult`, `setHotspotState()` resolves when the requested hotspot state is reported, and `requestVersionInfo()` waits for all chunks declared by the correlated response's `chunkCount`, `chunkIndex`, and `final` metadata. Legacy single-message responses complete immediately. Factory ASG27 declares build 27 in `version_info_1` and completes on its final `version_info_2` carrying the OTA URL; other legacy chunked responses complete on `version_info_3` after `version_info_1`. Missing final chunks time out rather than returning partial data after a quiet period.
|
|
511
573
|
|
|
512
574
|
Current Mentra Live builds advertise `wifiForgetResultVersion` and `savedWifiNetworksVersion` in `version_info_1`. Native queues calls while those capabilities are unknown, sharing one 15-second deadline for negotiation and response. A discovery timeout rejects and never guesses that the glasses are legacy. Only version 1 is supported: modern commands carry `protocolVersion: 1`, a nonempty request ID, and the glasses process session ID; legacy forget commands omit all three fields. Unknown or malformed versions fail closed. Correlated results must match the complete tuple and (for forget) exact SSID; unrelated `wifi_status` events never settle a modern request. Session changes and disconnects reject pending WiFi work instead of allowing stale responses to cross sessions.
|
|
513
575
|
|
package/android/build.gradle
CHANGED
|
@@ -186,6 +186,21 @@ apply from: expoModulesCorePlugin
|
|
|
186
186
|
applyKotlinExpoModulesCorePlugin()
|
|
187
187
|
useExpoPublishing()
|
|
188
188
|
|
|
189
|
+
// Hosts that embed @mentra/crust compile with Kotlin 2.3 (its QuickJS library
|
|
190
|
+
// requires it). Keep the public Maven artifact usable from Kotlin 2.1 builds:
|
|
191
|
+
// 2.2 metadata (a 2.1 compiler reads one version ahead; 2.2 is also the lowest
|
|
192
|
+
// language version that reads the 2.3 expo-modules-core classes compiled here),
|
|
193
|
+
// 2.1 stdlib APIs, and a kotlin-stdlib 2.1.20 dependency in the POM.
|
|
194
|
+
if (publicMavenSdk) {
|
|
195
|
+
kotlin {
|
|
196
|
+
coreLibrariesVersion = "2.1.20"
|
|
197
|
+
compilerOptions {
|
|
198
|
+
languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2
|
|
199
|
+
apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
189
204
|
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
190
205
|
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
191
206
|
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
@@ -129,7 +129,12 @@ android {
|
|
|
129
129
|
|
|
130
130
|
externalNativeBuild {
|
|
131
131
|
cmake {
|
|
132
|
-
|
|
132
|
+
// Google Play rejects 64-bit libraries whose ELF LOAD segments
|
|
133
|
+
// are not 16 KB aligned. NDK r27 (the React Native default)
|
|
134
|
+
// only aligns them with this flag; React Native's Gradle plugin
|
|
135
|
+
// adds it to the app module, never to library modules.
|
|
136
|
+
arguments '-DCMAKE_OBJECT_PATH_MAX=600', '-DANDROID_STL=c++_static',
|
|
137
|
+
'-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON'
|
|
133
138
|
}
|
|
134
139
|
}
|
|
135
140
|
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.content.IntentFilter
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.Handler
|
|
9
|
+
import android.os.Looper
|
|
10
|
+
import android.os.SystemClock
|
|
11
|
+
import android.util.Log
|
|
12
|
+
import org.json.JSONArray
|
|
13
|
+
import org.json.JSONObject
|
|
14
|
+
import java.io.BufferedWriter
|
|
15
|
+
import java.io.File
|
|
16
|
+
import java.security.MessageDigest
|
|
17
|
+
import java.security.SecureRandom
|
|
18
|
+
import java.util.UUID
|
|
19
|
+
|
|
20
|
+
/** ADB-only, debug-build framebuffer capture on the existing owning GATT connection.
|
|
21
|
+
*
|
|
22
|
+
* No display, lifecycle, firmware, memory-address, or arbitrary-command controls are exposed.
|
|
23
|
+
* Captures are private device files, not application events or cloud logs. Two matching reads
|
|
24
|
+
* are non-atomic panel-memory evidence, not proof of light emitted by the display.
|
|
25
|
+
*/
|
|
26
|
+
internal class NimoDiagnostics(
|
|
27
|
+
context: Context,
|
|
28
|
+
private val handler: Handler,
|
|
29
|
+
private val send: (ByteArray, () -> Unit) -> Boolean,
|
|
30
|
+
private val requestCanvasHold: ((() -> Unit) -> Unit) = { ready -> ready() },
|
|
31
|
+
private val releaseCanvasHold: (resume: Boolean) -> Unit = {},
|
|
32
|
+
) {
|
|
33
|
+
private val context = context.applicationContext
|
|
34
|
+
private val random = SecureRandom()
|
|
35
|
+
private var generation = 0L
|
|
36
|
+
private var maxWriteBytes = 0
|
|
37
|
+
private var registered = false
|
|
38
|
+
private var closed = false
|
|
39
|
+
private var active: Capture? = null
|
|
40
|
+
|
|
41
|
+
private enum class Stage { VERSION, READINESS, PING, GATE, PAGE }
|
|
42
|
+
private class Step(val stage: Stage, val request: NimoFramebufferProtocol.Request? = null) {
|
|
43
|
+
var timeout: Runnable? = null
|
|
44
|
+
}
|
|
45
|
+
private class Capture(val mode: String, val directory: File, val generation: Long) {
|
|
46
|
+
val started = SystemClock.elapsedRealtime()
|
|
47
|
+
val startedWallTime = System.currentTimeMillis()
|
|
48
|
+
val wire: BufferedWriter = File(directory, "wire.jsonl").bufferedWriter()
|
|
49
|
+
val nonces = mutableSetOf<Long>()
|
|
50
|
+
val banks = mutableListOf<NimoFramebufferProtocol.BankRead>()
|
|
51
|
+
val bankReceipts = JSONArray()
|
|
52
|
+
var wireCount = 0
|
|
53
|
+
var version: String? = null
|
|
54
|
+
var readiness: List<Int>? = null
|
|
55
|
+
var pending: Step? = null
|
|
56
|
+
var deadline: Runnable? = null
|
|
57
|
+
var currentBank: NimoFramebufferProtocol.BankRead? = null
|
|
58
|
+
var probeOk = false
|
|
59
|
+
var verifiedBanks = 0
|
|
60
|
+
var sampleState: NimoFramebufferProtocol.State? = null
|
|
61
|
+
var readStarted: Long? = null
|
|
62
|
+
var readFinished: Long? = null
|
|
63
|
+
var holdRequested = false
|
|
64
|
+
var holdReleased = false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private val receiver = object : BroadcastReceiver() {
|
|
68
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
69
|
+
requireMain()
|
|
70
|
+
if (intent?.action != ACTION) return
|
|
71
|
+
val mode = intent.getStringExtra("mode") ?: "probe"
|
|
72
|
+
if (mode !in listOf("probe", "capture", "sample", "held-capture", "held-sample")) {
|
|
73
|
+
Log.w(TAG, "Rejected unsupported mode")
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
start(mode)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fun connected(maxWriteBytes: Int) {
|
|
81
|
+
requireMain()
|
|
82
|
+
if (closed) return
|
|
83
|
+
disconnected()
|
|
84
|
+
this.maxWriteBytes = maxWriteBytes
|
|
85
|
+
try {
|
|
86
|
+
val filter = IntentFilter(ACTION)
|
|
87
|
+
if (Build.VERSION.SDK_INT >= 33) {
|
|
88
|
+
context.registerReceiver(receiver, filter, "android.permission.DUMP", handler, Context.RECEIVER_EXPORTED)
|
|
89
|
+
} else {
|
|
90
|
+
@Suppress("DEPRECATION")
|
|
91
|
+
context.registerReceiver(receiver, filter, "android.permission.DUMP", handler)
|
|
92
|
+
}
|
|
93
|
+
registered = true
|
|
94
|
+
Log.i(TAG, "ADB diagnostic receiver ready; write capacity=$maxWriteBytes")
|
|
95
|
+
} catch (error: RuntimeException) {
|
|
96
|
+
Log.w(TAG, "Diagnostic receiver unavailable: ${error.javaClass.simpleName}")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun disconnected() {
|
|
101
|
+
requireMain()
|
|
102
|
+
generation += 1
|
|
103
|
+
active?.let { finish(it, "GATT disconnected or connection generation changed", resumeHeldCanvas = false) }
|
|
104
|
+
maxWriteBytes = 0
|
|
105
|
+
if (registered) {
|
|
106
|
+
registered = false
|
|
107
|
+
try { context.unregisterReceiver(receiver) } catch (_: IllegalArgumentException) { }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fun close() {
|
|
112
|
+
requireMain()
|
|
113
|
+
disconnected()
|
|
114
|
+
closed = true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fun cancelHeldCapture(reason: String, resumeCanvas: Boolean = false) {
|
|
118
|
+
requireMain()
|
|
119
|
+
val capture = active ?: return
|
|
120
|
+
if (isHeld(capture.mode)) finish(capture, reason, resumeCanvas)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Called only for notifications from the owning NIMO response characteristic. */
|
|
124
|
+
fun onPacket(packet: ByteArray): Boolean {
|
|
125
|
+
requireMain()
|
|
126
|
+
val capture = active ?: return false
|
|
127
|
+
if (packet.size < 10 || packet[8].toInt() != 2) return false
|
|
128
|
+
val key = packet[9].toInt() and 255
|
|
129
|
+
if (key !in listOf(0x0B, 0x19, 0x15)) return false
|
|
130
|
+
try {
|
|
131
|
+
recordWire(capture, "rx", packet)
|
|
132
|
+
val step = capture.pending
|
|
133
|
+
when (key) {
|
|
134
|
+
0x0B -> {
|
|
135
|
+
val payload = NimoFramebufferProtocol.basicPayload(packet, key)
|
|
136
|
+
capture.version = if (payload.isNotEmpty()) payload.copyOfRange(1, payload.size).toString(Charsets.US_ASCII) else ""
|
|
137
|
+
NimoFramebufferProtocol.version(packet)
|
|
138
|
+
if (step?.stage == Stage.VERSION) {
|
|
139
|
+
completeStep(capture, step)
|
|
140
|
+
request(capture, Step(Stage.READINESS))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
0x19 -> {
|
|
144
|
+
capture.readiness = NimoFramebufferProtocol.readiness(packet)
|
|
145
|
+
val state = capture.readiness!!
|
|
146
|
+
Log.i(TAG, "Readiness status=${state[0]} ready=${state[1]} tws=${state[2]} peerGatt=${state[3]}")
|
|
147
|
+
if (step?.stage == Stage.READINESS) {
|
|
148
|
+
completeStep(capture, step)
|
|
149
|
+
request(capture, Step(Stage.PING, NimoFramebufferProtocol.Request(nonce(capture), 0, 0, 0)))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
0x15 -> {
|
|
153
|
+
require(step != null && step.request != null) { "Unexpected FBP1 response without a pending request" }
|
|
154
|
+
val reply = NimoFramebufferProtocol.reply(packet, step.request)
|
|
155
|
+
completeStep(capture, step)
|
|
156
|
+
when (step.stage) {
|
|
157
|
+
Stage.PING -> request(capture, Step(Stage.GATE, NimoFramebufferProtocol.Request(nonce(capture), 1, 0, 0)))
|
|
158
|
+
Stage.GATE -> {
|
|
159
|
+
// A complete 275-byte response must succeed before any bulk read is attempted.
|
|
160
|
+
capture.probeOk = true
|
|
161
|
+
if (isSample(capture.mode)) capture.sampleState = reply.after
|
|
162
|
+
Log.i(TAG, "FBP1 identity and full-page gate verified; mode=${capture.mode}")
|
|
163
|
+
if (capture.mode == "probe") finish(capture, null) else nextBank(capture)
|
|
164
|
+
}
|
|
165
|
+
Stage.PAGE -> acceptPage(capture, reply)
|
|
166
|
+
else -> throw IllegalArgumentException("FBP1 response arrived in an invalid phase")
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (error: Exception) {
|
|
171
|
+
finish(capture, "${error.javaClass.simpleName}: ${error.message?.take(160)}")
|
|
172
|
+
}
|
|
173
|
+
// Basic responses remain visible to the production handshake. Private FBP1 never enters
|
|
174
|
+
// the legacy assembler, whose response length convention is different.
|
|
175
|
+
return key == 0x15
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private fun start(mode: String) {
|
|
179
|
+
if (closed || !registered) return
|
|
180
|
+
if (active != null) { Log.w(TAG, "Capture already running; ignored duplicate request"); return }
|
|
181
|
+
val capture = try {
|
|
182
|
+
val root = File(context.filesDir, "nimo-framebuffer")
|
|
183
|
+
require(root.isDirectory || root.mkdirs()) { "Cannot create private capture directory" }
|
|
184
|
+
val directory = File(root, "${System.currentTimeMillis()}-${UUID.randomUUID().toString().take(8)}")
|
|
185
|
+
require(directory.mkdir()) { "Cannot create unique capture directory" }
|
|
186
|
+
Capture(mode, directory, generation)
|
|
187
|
+
} catch (error: Exception) {
|
|
188
|
+
Log.e(TAG, "Cannot begin private capture: ${error.javaClass.simpleName}")
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
active = capture
|
|
192
|
+
Log.i(TAG, "Starting $mode; files=${capture.directory.absolutePath}")
|
|
193
|
+
if (maxWriteBytes < 24) { finish(capture, "Write capacity below 24-byte diagnostic request"); return }
|
|
194
|
+
val budgetMs = when (mode) {
|
|
195
|
+
"held-sample" -> 15_000L
|
|
196
|
+
"held-capture" -> 55_000L
|
|
197
|
+
else -> 600_000L
|
|
198
|
+
}
|
|
199
|
+
val budgetDescription = when (mode) {
|
|
200
|
+
"held-sample" -> "15-second budget"
|
|
201
|
+
"held-capture" -> "55-second budget"
|
|
202
|
+
else -> "10-minute budget"
|
|
203
|
+
}
|
|
204
|
+
val deadline = Runnable {
|
|
205
|
+
if (active === capture) finish(capture, "Capture exceeded $budgetDescription")
|
|
206
|
+
}
|
|
207
|
+
capture.deadline = deadline
|
|
208
|
+
handler.postDelayed(deadline, budgetMs)
|
|
209
|
+
if (isHeld(mode)) {
|
|
210
|
+
capture.holdRequested = true
|
|
211
|
+
requestCanvasHold {
|
|
212
|
+
requireMain()
|
|
213
|
+
if (active === capture && capture.generation == generation) request(capture, Step(Stage.VERSION))
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
request(capture, Step(Stage.VERSION))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private fun request(capture: Capture, step: Step) {
|
|
221
|
+
if (active !== capture || capture.generation != generation) return
|
|
222
|
+
require(capture.pending == null) { "Only one diagnostic request may be in flight" }
|
|
223
|
+
val frame = when (step.stage) {
|
|
224
|
+
Stage.VERSION -> NimoFramebufferProtocol.queryFrame(0x0B)
|
|
225
|
+
Stage.READINESS -> NimoFramebufferProtocol.queryFrame(0x19)
|
|
226
|
+
else -> NimoFramebufferProtocol.requestFrame(requireNotNull(step.request))
|
|
227
|
+
}
|
|
228
|
+
capture.pending = step
|
|
229
|
+
try {
|
|
230
|
+
recordWire(capture, "tx", frame)
|
|
231
|
+
val accepted = send(frame) {
|
|
232
|
+
requireMain()
|
|
233
|
+
// A response may beat Android's write callback. Never arm an old step's timeout.
|
|
234
|
+
if (active === capture && capture.generation == generation && capture.pending === step) {
|
|
235
|
+
val timeout = Runnable {
|
|
236
|
+
if (active === capture && capture.pending === step) finish(capture, "${step.stage} response timeout")
|
|
237
|
+
}
|
|
238
|
+
step.timeout = timeout
|
|
239
|
+
handler.postDelayed(timeout, 3_000L)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!accepted && active === capture) finish(capture, "Owning GATT queue rejected diagnostic request")
|
|
243
|
+
} catch (error: Exception) {
|
|
244
|
+
finish(capture, "Diagnostic send failed: ${error.javaClass.simpleName}")
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private fun completeStep(capture: Capture, step: Step) {
|
|
249
|
+
step.timeout?.let(handler::removeCallbacks)
|
|
250
|
+
capture.pending = null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private fun nextBank(capture: Capture) {
|
|
254
|
+
if (isSample(capture.mode) && capture.banks.size == 1) { finish(capture, null); return }
|
|
255
|
+
if (capture.banks.size == 4) { finish(capture, null); return }
|
|
256
|
+
val bank = if (isSample(capture.mode)) requireNotNull(capture.sampleState).prepared else capture.banks.size / 2
|
|
257
|
+
val read = NimoFramebufferProtocol.BankRead(nonce(capture), bank)
|
|
258
|
+
capture.currentBank = read
|
|
259
|
+
capture.readStarted = SystemClock.elapsedRealtime() - capture.started
|
|
260
|
+
request(capture, Step(Stage.PAGE, NimoFramebufferProtocol.Request(read.nonce, 1, bank, 0)))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private fun acceptPage(capture: Capture, reply: NimoFramebufferProtocol.Reply) {
|
|
264
|
+
val read = requireNotNull(capture.currentBank)
|
|
265
|
+
if (isSample(capture.mode)) {
|
|
266
|
+
require(reply.before == capture.sampleState) { "Sample metadata differs from page gate" }
|
|
267
|
+
}
|
|
268
|
+
read.add(reply)
|
|
269
|
+
if (read.pageCount % 63 == 0) Log.i(TAG, "bank=${read.bank} pass=${capture.banks.size % 2 + 1} pages=${read.pageCount}/315")
|
|
270
|
+
if (read.pageCount < NimoFramebufferProtocol.PAGE_COUNT) {
|
|
271
|
+
request(capture, Step(Stage.PAGE, NimoFramebufferProtocol.Request(read.nonce, 1, read.bank, read.pageCount)))
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
val pass = capture.banks.size % 2 + 1
|
|
275
|
+
val filename = "bank${read.bank}-pass$pass.bin"
|
|
276
|
+
val bytes = read.bytes()
|
|
277
|
+
capture.readFinished = SystemClock.elapsedRealtime() - capture.started
|
|
278
|
+
File(capture.directory, filename).writeBytes(bytes)
|
|
279
|
+
capture.bankReceipts.put(JSONObject().put("bank", read.bank).put("pass", pass).put("nonce", read.nonce)
|
|
280
|
+
.put("file", filename).put("page_count", read.pageCount).put("sha256", sha256(bytes))
|
|
281
|
+
.put("metadata", metadata(requireNotNull(read.metadata))))
|
|
282
|
+
capture.banks.add(read)
|
|
283
|
+
capture.currentBank = null
|
|
284
|
+
if (pass == 2) {
|
|
285
|
+
NimoFramebufferProtocol.verifyPair(capture.banks[capture.banks.size - 2], read)
|
|
286
|
+
capture.verifiedBanks += 1
|
|
287
|
+
Log.i(TAG, "Static pair verified for bank=${read.bank}; non-atomic panel memory only")
|
|
288
|
+
}
|
|
289
|
+
nextBank(capture)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun finish(capture: Capture, error: String?, resumeHeldCanvas: Boolean = true) {
|
|
293
|
+
if (active !== capture) return
|
|
294
|
+
active = null
|
|
295
|
+
capture.deadline?.let(handler::removeCallbacks)
|
|
296
|
+
capture.pending?.timeout?.let(handler::removeCallbacks)
|
|
297
|
+
capture.pending = null
|
|
298
|
+
if (capture.holdRequested && !capture.holdReleased) {
|
|
299
|
+
capture.holdReleased = true
|
|
300
|
+
releaseCanvasHold(resumeHeldCanvas)
|
|
301
|
+
}
|
|
302
|
+
var failure = error
|
|
303
|
+
try { capture.wire.close() } catch (_: Exception) { failure = failure ?: "Wire evidence could not be flushed" }
|
|
304
|
+
val complete = failure == null && !isSample(capture.mode) && capture.mode != "probe" &&
|
|
305
|
+
capture.verifiedBanks == 2 && capture.banks.size == 4
|
|
306
|
+
val sampleComplete = failure == null && isSample(capture.mode) && capture.banks.size == 1
|
|
307
|
+
val receipt = JSONObject().put("mode", capture.mode).put("ok", complete)
|
|
308
|
+
.put("sample_ok", sampleComplete)
|
|
309
|
+
.put("probe_ok", capture.probeOk && failure == null).put("error", failure ?: JSONObject.NULL)
|
|
310
|
+
.put("started_at_unix_ms", capture.startedWallTime).put("elapsed_ms", SystemClock.elapsedRealtime() - capture.started)
|
|
311
|
+
.put("runtime_version", capture.version ?: JSONObject.NULL)
|
|
312
|
+
.put("declared_installed_artifact_sha256", NimoFramebufferProtocol.ARTIFACT_SHA256)
|
|
313
|
+
.put("artifact_provenance", "Previously installed local diagnostic artifact; runtime SHA-256 is not measured")
|
|
314
|
+
.put("runtime_digest_measured", false)
|
|
315
|
+
.put("evidence_kind", if (isSample(capture.mode)) "non_atomic_single_pass" else "non_atomic_static_scene")
|
|
316
|
+
.put("pixel_stability_verified", complete)
|
|
317
|
+
.put("atomic", false).put("optical", false).put("optical_output_verified", false)
|
|
318
|
+
.put("notification_source", JSONObject().put("service", "7033").put("characteristic", "2022")
|
|
319
|
+
.put("transport", "Mentra owning Android GATT"))
|
|
320
|
+
.put("write_capacity", maxWriteBytes).put("width", NimoFramebufferProtocol.WIDTH)
|
|
321
|
+
.put("height", NimoFramebufferProtocol.HEIGHT).put("format", "packed4_high_nibble_first")
|
|
322
|
+
.put("wire_file", "wire.jsonl").put("wire_events", capture.wireCount).put("banks", capture.bankReceipts)
|
|
323
|
+
.put("static_pairs_verified", capture.verifiedBanks)
|
|
324
|
+
if (isSample(capture.mode)) {
|
|
325
|
+
receipt.put("selection_policy", "prepared_at_page_gate")
|
|
326
|
+
.put("selected_bank", capture.sampleState?.prepared ?: JSONObject.NULL)
|
|
327
|
+
.put("selection_metadata", capture.sampleState?.let(::metadata) ?: JSONObject.NULL)
|
|
328
|
+
.put("read_started_elapsed_ms", capture.readStarted ?: JSONObject.NULL)
|
|
329
|
+
.put("read_finished_elapsed_ms", capture.readFinished ?: JSONObject.NULL)
|
|
330
|
+
}
|
|
331
|
+
capture.readiness?.let { state ->
|
|
332
|
+
receipt.put("readiness", JSONObject().put("status", state[0]).put("ready", state[1])
|
|
333
|
+
.put("tws", state[2]).put("peer_gatt", state[3]))
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
File(capture.directory, "session.json").writeText(receipt.toString(2) + "\n")
|
|
337
|
+
Log.i(TAG, "Finished mode=${capture.mode} ok=$complete sample_ok=$sampleComplete probe_ok=${capture.probeOk && failure == null} error=${failure ?: "none"}; files=${capture.directory.absolutePath}")
|
|
338
|
+
} catch (_: Exception) {
|
|
339
|
+
Log.e(TAG, "Capture receipt could not be written; do not treat this capture as verified")
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private fun nonce(capture: Capture): Long {
|
|
344
|
+
var value: Long
|
|
345
|
+
do { value = random.nextInt().toLong() and 0xFFFFFFFFL } while (!capture.nonces.add(value))
|
|
346
|
+
return value
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private fun recordWire(capture: Capture, direction: String, packet: ByteArray) {
|
|
350
|
+
require(packet.size <= 512 && capture.wireCount < 2700) { "Diagnostic wire evidence exceeded its bound" }
|
|
351
|
+
capture.wire.write(JSONObject().put("direction", direction).put("elapsed_ms", SystemClock.elapsedRealtime() - capture.started)
|
|
352
|
+
.put("hex", NimoFramebufferProtocol.hex(packet)).toString())
|
|
353
|
+
capture.wire.newLine()
|
|
354
|
+
capture.wireCount += 1
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private fun metadata(state: NimoFramebufferProtocol.State) = JSONObject().put("draw", state.draw)
|
|
358
|
+
.put("prepared", state.prepared).put("dirty", state.dirty).put("pending", state.pending)
|
|
359
|
+
private fun sha256(bytes: ByteArray) = NimoFramebufferProtocol.hex(MessageDigest.getInstance("SHA-256").digest(bytes))
|
|
360
|
+
private fun requireMain() { check(Looper.myLooper() == handler.looper) { "NIMO diagnostics must run on its owning handler" } }
|
|
361
|
+
private fun isHeld(mode: String) = mode == "held-capture" || mode == "held-sample"
|
|
362
|
+
private fun isSample(mode: String) = mode == "sample" || mode == "held-sample"
|
|
363
|
+
|
|
364
|
+
companion object {
|
|
365
|
+
private const val TAG = "NimoFramebuffer"
|
|
366
|
+
private const val ACTION = "com.mentra.NIMO_FRAMEBUFFER_CAPTURE"
|
|
367
|
+
}
|
|
368
|
+
}
|