@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.
Files changed (191) hide show
  1. package/README.md +72 -10
  2. package/android/build.gradle +15 -0
  3. package/android/lc3Lib/build.gradle +6 -1
  4. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
  5. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
  7. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
  8. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
  9. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
  11. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
  15. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
  16. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
  17. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
  18. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
  19. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
  22. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
  24. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +4 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
  29. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
  30. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
  31. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
  32. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
  33. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
  34. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
  35. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +247 -0
  36. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
  37. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
  39. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
  40. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
  41. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
  42. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
  43. package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
  44. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
  45. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
  46. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
  47. package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
  48. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
  49. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
  50. package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
  51. package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
  52. package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
  53. package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
  54. package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
  55. package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
  56. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
  57. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
  58. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
  59. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
  60. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
  61. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
  62. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
  63. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
  64. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
  65. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
  66. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
  67. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
  68. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
  69. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +233 -0
  70. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
  71. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
  72. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
  73. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
  74. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
  75. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
  76. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
  77. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
  78. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
  79. package/build/BluetoothSdk.types.d.ts +16 -0
  80. package/build/BluetoothSdk.types.d.ts.map +1 -1
  81. package/build/BluetoothSdk.types.js +1 -0
  82. package/build/BluetoothSdk.types.js.map +1 -1
  83. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  84. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  85. package/build/_private/BluetoothSdkModule.js +5 -1
  86. package/build/_private/BluetoothSdkModule.js.map +1 -1
  87. package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
  88. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  89. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  90. package/build/_private/nativeLogConsole.d.ts +19 -0
  91. package/build/_private/nativeLogConsole.d.ts.map +1 -1
  92. package/build/_private/nativeLogConsole.js +12 -0
  93. package/build/_private/nativeLogConsole.js.map +1 -1
  94. package/build/generated/changelogCatalog.d.ts +5 -2
  95. package/build/generated/changelogCatalog.d.ts.map +1 -1
  96. package/build/generated/changelogCatalog.js +6 -2
  97. package/build/generated/changelogCatalog.js.map +1 -1
  98. package/build/generated/releaseMetadata.js +6 -6
  99. package/build/generated/releaseMetadata.js.map +1 -1
  100. package/build/index.d.ts.map +1 -1
  101. package/build/index.js +1 -0
  102. package/build/index.js.map +1 -1
  103. package/build/ota-transport/index.d.ts +4 -0
  104. package/build/ota-transport/index.d.ts.map +1 -1
  105. package/build/ota-transport/index.js +2 -0
  106. package/build/ota-transport/index.js.map +1 -1
  107. package/ios/BluetoothSdkModule.swift +17 -4
  108. package/ios/MentraBluetoothSDK.podspec +3 -2
  109. package/ios/Source/BluetoothBackgroundMode.swift +32 -0
  110. package/ios/Source/BluetoothSdkDefaults.swift +7 -2
  111. package/ios/Source/Bridge.swift +15 -2
  112. package/ios/Source/DeviceManager.swift +102 -66
  113. package/ios/Source/DeviceStore.swift +26 -7
  114. package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
  115. package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
  116. package/ios/Source/MentraBluetoothSDK.swift +313 -54
  117. package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
  118. package/ios/Source/internal/BluetoothAvailability.swift +6 -0
  119. package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
  120. package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
  121. package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
  122. package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
  123. package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
  124. package/ios/Source/requests/DisplayRequests.swift +1 -1
  125. package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
  126. package/ios/Source/sgcs/Ar99.swift +1 -2
  127. package/ios/Source/sgcs/G1.swift +68 -38
  128. package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
  129. package/ios/Source/sgcs/G2.swift +261 -247
  130. package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
  131. package/ios/Source/sgcs/Mach1.swift +19 -7
  132. package/ios/Source/sgcs/MentraLive.swift +272 -197
  133. package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
  134. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
  135. package/ios/Source/sgcs/MentraNex.swift +8 -31
  136. package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
  137. package/ios/Source/sgcs/Nimo.swift +280 -408
  138. package/ios/Source/sgcs/NimoCanvasCodec.swift +272 -0
  139. package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
  140. package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
  141. package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
  142. package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
  143. package/ios/Source/sgcs/SGCManager.swift +25 -1
  144. package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
  145. package/ios/Source/status/DeviceStatus.swift +1 -0
  146. package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
  147. package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
  148. package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
  149. package/ios/Source/utils/G1Text.swift +2 -24
  150. package/ios/Source/utils/G1TextSanitizer.swift +27 -0
  151. package/ios/Source/utils/G2Text.swift +11 -0
  152. package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
  153. package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
  154. package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
  155. package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
  156. package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
  157. package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
  158. package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
  159. package/ios/Tests/DashboardRenderingTests.swift +31 -0
  160. package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
  161. package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
  162. package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
  163. package/ios/Tests/G1PairingTests.swift +162 -0
  164. package/ios/Tests/G1TextSanitizerTests.swift +32 -0
  165. package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
  166. package/ios/Tests/G2TextTests.swift +10 -0
  167. package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
  168. package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
  169. package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
  170. package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
  171. package/ios/Tests/NimoCanvasCodecTests.swift +217 -0
  172. package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
  173. package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
  174. package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
  175. package/ios/Tests/NimoDiscoveryTests.swift +79 -0
  176. package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
  177. package/ios/Tests/VersionInfoCommandTests.swift +194 -0
  178. package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
  179. package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
  180. package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
  181. package/package.json +2 -2
  182. package/plugin/build/withIos.d.ts +2 -0
  183. package/plugin/build/withIos.js +12 -1
  184. package/src/BluetoothSdk.types.ts +15 -0
  185. package/src/_private/BluetoothSdkModule.ts +10 -1
  186. package/src/_private/MentraLocalNetworkModule.ts +28 -0
  187. package/src/_private/nativeLogConsole.ts +19 -0
  188. package/src/generated/changelogCatalog.ts +6 -2
  189. package/src/generated/releaseMetadata.ts +6 -6
  190. package/src/index.ts +1 -0
  191. 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 three usage events to Mentra's PostHog project by default so
221
- Mentra can understand SDK adoption, successful glasses connections, and
222
- enterprise device deployments:
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, and `event_kind`. Every event also carries host build facts:
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 credentials, microphone data, photos, or
312
- transcripts. PostHog receives a locally generated anonymous SDK install id as
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
- $CONNECTION_STATUS$` status header and places non-empty content below it after a
451
- blank line. Pass the exact empty string to reset the dashboard to the status
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
 
@@ -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
- arguments '-DCMAKE_OBJECT_PATH_MAX=600', '-DANDROID_STL=c++_static'
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
+ }