@mentra/bluetooth-sdk 0.1.21-beta.5 → 3.1.0-dev.7

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 (114) hide show
  1. package/README.md +38 -2
  2. package/android/build.gradle +7 -4
  3. package/android/lc3Lib/build.gradle +18 -4
  4. package/android/silero/build.gradle +9 -1
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +39 -3
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +198 -17
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +112 -8
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
  12. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
  13. package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
  20. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1174 -191
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
  24. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
  26. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
  27. package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  28. package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  29. package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
  33. package/build/BluetoothSdk.types.d.ts +61 -2
  34. package/build/BluetoothSdk.types.d.ts.map +1 -1
  35. package/build/BluetoothSdk.types.js.map +1 -1
  36. package/build/_internal.d.ts +2 -0
  37. package/build/_internal.d.ts.map +1 -1
  38. package/build/_internal.js +2 -0
  39. package/build/_internal.js.map +1 -1
  40. package/build/_private/BluetoothSdkModule.d.ts +4 -1
  41. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  42. package/build/_private/BluetoothSdkModule.js +1 -1
  43. package/build/_private/BluetoothSdkModule.js.map +1 -1
  44. package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
  45. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  46. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  47. package/build/_private/sdkOtaManifest.d.ts +3 -12
  48. package/build/_private/sdkOtaManifest.d.ts.map +1 -1
  49. package/build/_private/sdkOtaManifest.js +5 -17
  50. package/build/_private/sdkOtaManifest.js.map +1 -1
  51. package/build/generated/releaseMetadata.d.ts +12 -0
  52. package/build/generated/releaseMetadata.d.ts.map +1 -0
  53. package/build/generated/releaseMetadata.js +10 -0
  54. package/build/generated/releaseMetadata.js.map +1 -0
  55. package/build/index.d.ts +1 -1
  56. package/build/index.d.ts.map +1 -1
  57. package/build/index.js +10 -2
  58. package/build/index.js.map +1 -1
  59. package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
  60. package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
  61. package/build/ota-server/MentraOtaServer.types.js +2 -0
  62. package/build/ota-server/MentraOtaServer.types.js.map +1 -0
  63. package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
  64. package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
  65. package/build/ota-server/MentraOtaServerModule.js +3 -0
  66. package/build/ota-server/MentraOtaServerModule.js.map +1 -0
  67. package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
  68. package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
  69. package/build/ota-server/MentraOtaServerModule.web.js +12 -0
  70. package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
  71. package/build/ota-server/index.d.ts +3 -0
  72. package/build/ota-server/index.d.ts.map +1 -0
  73. package/build/ota-server/index.js +3 -0
  74. package/build/ota-server/index.js.map +1 -0
  75. package/expo-module.config.json +3 -2
  76. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  77. package/ios/BluetoothSdkModule.swift +16 -2
  78. package/ios/LocalIPv4.swift +150 -0
  79. package/ios/LocalOtaServer.swift +309 -0
  80. package/ios/MentraBluetoothSDK.podspec +5 -1
  81. package/ios/MentraOtaServerModule.swift +147 -0
  82. package/ios/MentraPhotoReceiverModule.swift +1 -81
  83. package/ios/Source/BluetoothSdkDefaults.swift +4 -1
  84. package/ios/Source/Bridge.swift +47 -3
  85. package/ios/Source/DeviceManager.swift +135 -10
  86. package/ios/Source/DeviceStore.swift +2 -0
  87. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  88. package/ios/Source/MentraBluetoothSDK.swift +74 -6
  89. package/ios/Source/OtaManifest.swift +5 -7
  90. package/ios/Source/audio/AudioModels.swift +17 -0
  91. package/ios/Source/camera/CameraModels.swift +1 -0
  92. package/ios/Source/events/BluetoothEvents.swift +27 -0
  93. package/ios/Source/sgcs/MentraLive.swift +439 -30
  94. package/ios/Source/sgcs/SGCManager.swift +11 -0
  95. package/ios/Source/status/DeviceStatus.swift +23 -0
  96. package/ios/Source/types/DeviceModels.swift +23 -2
  97. package/ios/Source/utils/BleJsonCompact.swift +73 -71
  98. package/package.json +5 -1
  99. package/plugin/build/withIos.d.ts +3 -1
  100. package/plugin/build/withIos.js +22 -10
  101. package/scripts/verify-release-package.mjs +34 -0
  102. package/scripts/write-release-metadata.mjs +154 -0
  103. package/scripts/write-release-metadata.test.mjs +29 -0
  104. package/src/BluetoothSdk.types.ts +72 -3
  105. package/src/_internal.ts +2 -0
  106. package/src/_private/BluetoothSdkModule.ts +5 -5
  107. package/src/_private/MentraLocalNetworkModule.ts +1 -1
  108. package/src/_private/sdkOtaManifest.ts +5 -17
  109. package/src/generated/releaseMetadata.ts +20 -0
  110. package/src/index.ts +19 -2
  111. package/src/ota-server/MentraOtaServer.types.ts +10 -0
  112. package/src/ota-server/MentraOtaServerModule.ts +23 -0
  113. package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
  114. package/src/ota-server/index.ts +2 -0
package/README.md CHANGED
@@ -303,6 +303,8 @@ console.log(forgetStatus.state)
303
303
  const hotspotStatus = await BluetoothSdk.setHotspotState(true)
304
304
  console.log(hotspotStatus.state)
305
305
 
306
+ await BluetoothSdk.setWifiAdbState(true) // Mentra Live Wi-Fi ADB (wireless debugging)
307
+
306
308
  const galleryAck = await BluetoothSdk.setGalleryModeEnabled(true)
307
309
  console.log(galleryAck.status)
308
310
  await BluetoothSdk.setGalleryModeEnabled(false)
@@ -328,6 +330,8 @@ Settings commands that return `SettingsAckSuccessEvent` reject when the ASG repo
328
330
  WiFi, hotspot, and version-info commands resolve from the ASG response path, not local dispatch:
329
331
  `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()` resolves when that SSID is no longer connected, `setHotspotState()` resolves when the requested hotspot state is reported, and `requestVersionInfo()` resolves from the ASG `version_info` response instead of local store changes.
330
332
 
333
+ The SDK automatically sends the phone wall clock once shortly after a glasses connection becomes ready. It waits for the initial command burst to drain before timestamping the command so startup queue delay does not become clock skew. The once-per-connection guard resets after disconnect, so every successful reconnect synchronizes again. The SDK does not periodically verify or correct clock skew during a long-lived connection; apps that require periodic reconciliation can compare `requestVersionInfo().systemTimeMs` with the phone clock.
334
+
331
335
  React Native narrows returned values to success shapes where the raw listener event can also report errors:
332
336
 
333
337
  | API | Returned Value After `await` | Error Path |
@@ -335,6 +339,7 @@ React Native narrows returned values to success shapes where the raw listener ev
335
339
  | `requestPhoto(...)` | Terminal `PhotoSuccessResponseEvent` with `state: "success"` after capture and delivery finish. `uploadUrl` is always present; webhook JSON metadata such as `photoUrl`, `statusUrl`, `contentType`, or `fileSizeBytes` is included when the receiver returns it. | Rejects when raw `photo_response.state === "error"`, the SDK cannot send the command, or the terminal photo response times out. |
336
340
  | `startVideoRecording(...)` | `VideoRecordingStartedStatusEvent` with `success: true` and `status: "recording_started"`. | Rejects on `success: false` statuses such as `already_recording`, send failure, or timeout. |
337
341
  | `stopVideoRecording(...)` | `VideoRecordingStoppedStatusEvent` with `success: true` and `status: "recording_stopped"`. When a webhook URL is supplied, resolves after the video upload succeeds. | Rejects on `success: false` statuses such as `not_recording`, webhook upload failure, send failure, or timeout. |
342
+ | `queryVideoRecordingStatus(requestId)` | `VideoRecordingStatusEvent` with the current `recording` state and elapsed `duration_ms` when available. | Rejects when disconnected, another video command uses the same request ID, or the response times out. |
338
343
  | `rgbLedControl(...)` | `RgbLedControlSuccessResponseEvent` with `state: "success"`. | Rejects when raw `rgb_led_control_response.state === "error"` or the response times out. |
339
344
  | `checkForOtaUpdate()` | `boolean`, true when the configured OTA manifest has an ASG APK, MTK, or BES update for the connected glasses; false only when the manifest was checked successfully and no update is available. | Rejects when the glasses are disconnected, version info is unavailable, the manifest cannot be fetched, or the manifest response is invalid/missing required ASG app version fields. |
340
345
 
@@ -346,6 +351,10 @@ Android and iOS async APIs use `BluetoothSdkException` / `BluetoothSdkError` for
346
351
 
347
352
  Mentra Live firmware owns the OTA flow. The SDK mirrors the MentraOS app commands and events:
348
353
 
354
+ - `setOtaVersionUrl(url)` selects the manifest used by subsequent checks and installs. Use this
355
+ for a customer-controlled internal update server.
356
+ - `getOtaVersionUrl()` returns the selected manifest, or the SDK-version-pinned Mentra manifest
357
+ when no override was supplied.
349
358
  - `checkForOtaUpdate()` fetches the configured manifest and resolves with `true` when an ASG APK, MTK, or BES update is available.
350
359
  - `startOtaUpdate()` sends `ota_start` with the same configured manifest URL and resolves with the ASG start ack after your app presents the update and the user accepts it.
351
360
 
@@ -360,6 +369,9 @@ prompt for an update the glasses cannot install.
360
369
  ```ts
361
370
  import BluetoothSdk from '@mentra/bluetooth-sdk'
362
371
 
372
+ // Optional: point both the phone-side check and glasses install at an internal server.
373
+ BluetoothSdk.setOtaVersionUrl('https://updates.example.internal/mentra-live/version.json')
374
+
363
375
  BluetoothSdk.addListener('ota_status', (event) => {
364
376
  console.log(`OTA ${event.status}: ${event.overall_percent}%`)
365
377
  })
@@ -374,8 +386,27 @@ if (hasUpdate) {
374
386
  }
375
387
  ```
376
388
 
389
+ Bluetooth SDK releases also publish
390
+ `bluetooth-sdk-<sdkVersion>-ota-bundle.zip` beside the default manifest. The archive contains
391
+ `version.template.json`, every referenced ASG/MTK/BES artifact, `SHA256SUMS`, and a dependency-free
392
+ configuration script. After unpacking it, generate the final manifest for its exact hosting URL:
393
+
394
+ ```sh
395
+ node configure.mjs https://updates.example.internal/mentra-live/version.json
396
+ ```
397
+
398
+ Host the entire directory at that location, then pass the same URL to `setOtaVersionUrl(...)`.
399
+ The generated `version.json` contains absolute internal artifact URLs, so it works with Mentra Live
400
+ ASG build 39 and newer as well as future firmware. Re-run the command if the hosted directory moves.
401
+
402
+ Firmware before ASG build 39 ignores `ota_start.ota_version_url` and must first be updated through
403
+ its legacy or factory-supported path. OTA remains host-driven: the SDK does not check or install
404
+ automatically.
405
+
377
406
  OTA requires Mentra Live glasses firmware that supports the ASG OTA protocol and network access from the glasses. During install, normal BLE traffic can be interrupted and the glasses may restart; keep the app connected and avoid sending unrelated commands until `ota_status.status` is `complete` or `failed`.
378
407
 
408
+ Mentra Live also rejects `ota_start` before acknowledgement when its known battery level is below 5%, emitting a failed `ota_status` with `error_message: "battery_low"`. Unknown battery state remains fail-open, so apps should keep their own (typically stricter) user-facing battery policy.
409
+
379
410
  ## Photo Upload
380
411
 
381
412
  ```ts
@@ -401,7 +432,7 @@ Use `setCameraFov({fov, roiPosition})` to configure Mentra Live camera field of
401
432
 
402
433
  The missing/factory persistent FOV base is 102 degrees with centered ROI; existing saved values are not migrated. Use `setCameraFovOverride({leaseId, fov, roiPosition, ttlMs})` for a memory-only crop and `releaseCameraFovOverride(leaseId)` to restore the persistent base. A stale release is a safe no-op, and refreshing the same lease/config extends its TTL without another HAL restart.
403
434
 
404
- `startVideoRecording(...)` resolves from the ASG `video_recording_status` event whose status is `recording_started`. `stopVideoRecording(...)` without a webhook resolves from `recording_stopped`; with a webhook it waits for `recording_stopped` plus a video `media_success`, and rejects on `media_error`. Raw `video_recording_status`, `media_success`, and `media_error` events remain available through listeners.
435
+ `startVideoRecording(...)` resolves from the ASG `video_recording_status` event whose status is `recording_started`. `stopVideoRecording(...)` without a webhook resolves from `recording_stopped`; with a webhook it waits for `recording_stopped` plus a video `media_success`, and rejects on `media_error`. Use `queryVideoRecordingStatus(requestId)` after a timeout, reconnect, or app resume to fetch the glasses' current `recording` state and elapsed `duration_ms`. Raw `video_recording_status`, `media_success`, and `media_error` events remain available through listeners.
405
436
 
406
437
  ## Streaming
407
438
 
@@ -438,6 +469,9 @@ export function HardwareEventLogger() {
438
469
  console.log(event.sampleRate, event.bitsPerSample, event.channels, event.encoding)
439
470
  console.log(event.pcm)
440
471
  })
472
+ useBluetoothEvent('mic_health', (event) => {
473
+ console.log(event.reason, event.sequenceGapEvents, event.decodeFailures)
474
+ })
441
475
 
442
476
  return null
443
477
  }
@@ -445,10 +479,12 @@ export function HardwareEventLogger() {
445
479
 
446
480
  For non-React modules, `BluetoothSdk.addListener(...)` is the low-level subscription API. Keep the returned subscription and call `remove()` when the listener is no longer needed.
447
481
 
448
- Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `wifi_scan_result`, `hotspot_status_change`, `photo_status`, `photo_response`, `gallery_status`, `settings_ack`, `version_info`, `stream_status`, `ota_start_ack`, `ota_status`, `mic_pcm`, `mic_lc3`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
482
+ Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `wifi_scan_result`, `hotspot_status_change`, `photo_status`, `photo_response`, `gallery_status`, `settings_ack`, `version_info`, `stream_status`, `ota_start_ack`, `ota_status`, `mic_pcm`, `mic_lc3`, `mic_health`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
449
483
 
450
484
  React Native event payload fields usually use camelCase. OTA events intentionally mirror the glasses firmware field names, such as `overall_percent` and `version_name`. For example, `touch_event` includes `gestureName`, `wifi_scan_result` includes `networks` and `scanComplete`, `version_info` matches the `requestVersionInfo()` result shape, `photo_response` success includes `uploadUrl` and may include webhook-returned `photoUrl`, `statusUrl`, `contentType`, and `fileSizeBytes`, and `gallery_status` includes `hasContent`, `cameraBusy`, and optional `cameraBusyReason`. `photo_status` reports intermediate photo states such as `accepted`, `queued`, `configuring`, `capturing`, `captured`, `compressing`, `ble_fallback_compression`, `uploading`, `ready_for_transfer`, `transferring`, and `failed`; the `configuring` event includes `resolvedConfig` with the effective JPEG dimensions, quality, requested size, transfer method, compression, and manual exposure fields when present. The `capturing` event may include `requestedCaptureConfig` and `meteredPreview`; the `captured` event may include `captureMetadata` with the HAL-applied exposure, ISO, frame duration, and AE state. `mic_pcm` includes `sampleRate`, `bitsPerSample`, `channels`, and `encoding`; `mic_lc3` includes `sampleRate`, `channels`, `encoding`, `frameDurationMs`, `frameSizeBytes`, `bitrate`, and `packetizedFromGlasses`.
451
485
 
486
+ `mic_health` emits a snapshot when native code detects a glasses LC3 `sequence_gap` or `decode_failure`. Each event includes cumulative `sequenceGapEvents` and `decodeFailures` counters for the current connection plus `lastLc3ReceivedAt`, `lastPcmProducedAt`, and `timestamp` in Unix milliseconds when available. These failures occur before React Native receives PCM and cannot be reconstructed from `mic_pcm` alone. The existing `mic_pcm` and `mic_lc3` streams remain unchanged.
487
+
452
488
  Photo status metadata is tied to the capture stage where the glasses know it:
453
489
 
454
490
  | Status | Optional metadata | Meaning |
@@ -379,10 +379,13 @@ afterEvaluate {
379
379
  repositories {
380
380
  maven {
381
381
  name = 'sonatypeCentral'
382
- url = uri('https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/')
383
- credentials {
384
- username = centralUsername()
385
- password = centralPassword()
382
+ def repositoryUrl = System.getenv('MENTRA_MAVEN_STAGING_REPOSITORY_URL') ?: 'https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/'
383
+ url = uri(repositoryUrl)
384
+ if (repositoryUrl.startsWith('http')) {
385
+ credentials {
386
+ username = centralUsername()
387
+ password = centralPassword()
388
+ }
386
389
  }
387
390
  }
388
391
  }
@@ -111,6 +111,17 @@ android {
111
111
 
112
112
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
113
113
 
114
+ ndk {
115
+ // Honor reactNativeArchitectures so CI/staging ABI filters apply.
116
+ // Without this, CMake still builds every ABI on an arm64-only job.
117
+ def abiList = (findProperty("reactNativeArchitectures") ?: "armeabi-v7a,arm64-v8a,x86,x86_64")
118
+ .toString()
119
+ .split(",")
120
+ .collect { it.trim() }
121
+ .findAll { !it.isEmpty() }
122
+ abiFilters(*abiList)
123
+ }
124
+
114
125
  externalNativeBuild {
115
126
  cmake {
116
127
  arguments '-DCMAKE_OBJECT_PATH_MAX=600', '-DANDROID_STL=c++_static'
@@ -170,10 +181,13 @@ afterEvaluate {
170
181
  repositories {
171
182
  maven {
172
183
  name = 'sonatypeCentral'
173
- url = uri('https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/')
174
- credentials {
175
- username = centralUsername()
176
- password = centralPassword()
184
+ def repositoryUrl = System.getenv('MENTRA_MAVEN_STAGING_REPOSITORY_URL') ?: 'https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/'
185
+ url = uri(repositoryUrl)
186
+ if (repositoryUrl.startsWith('http')) {
187
+ credentials {
188
+ username = centralUsername()
189
+ password = centralPassword()
190
+ }
177
191
  }
178
192
  }
179
193
  }
@@ -16,7 +16,15 @@ android {
16
16
  targetSdkVersion 36
17
17
 
18
18
  ndk {
19
- abiFilters "armeabi-v7a", "x86", "x86_64", "arm64-v8a"
19
+ // Honor reactNativeArchitectures (dev CI is arm64-only; staging
20
+ // production sets all four ABIs) so this module does not keep
21
+ // compiling every ABI on a filtered build.
22
+ def abiList = (findProperty("reactNativeArchitectures") ?: "armeabi-v7a,arm64-v8a,x86,x86_64")
23
+ .toString()
24
+ .split(",")
25
+ .collect { it.trim() }
26
+ .findAll { !it.isEmpty() }
27
+ abiFilters(*abiList)
20
28
  }
21
29
  }
22
30
 
@@ -286,6 +286,10 @@ class BluetoothSdkModule : Module() {
286
286
  sendEvent("mic_lc3", event.toMap())
287
287
  }
288
288
 
289
+ override fun onMicHealth(event: MicHealthEvent) {
290
+ sendEvent("mic_health", event.values)
291
+ }
292
+
289
293
  override fun onLocalTranscription(event: LocalTranscriptionEvent) {
290
294
  sendEvent("local_transcription", event.values)
291
295
  }
@@ -355,6 +359,9 @@ class BluetoothSdkModule : Module() {
355
359
  "settings_ack",
356
360
  "version_info",
357
361
  "pair_failure",
362
+ "pairing_info",
363
+ "entering_pairing_mode",
364
+ "owner_replaced",
358
365
  "audio_pairing_needed",
359
366
  "audio_connected",
360
367
  "audio_disconnected",
@@ -365,6 +372,7 @@ class BluetoothSdkModule : Module() {
365
372
  "ws_bin",
366
373
  "mic_pcm",
367
374
  "mic_lc3",
375
+ "mic_health",
368
376
  "stream_status",
369
377
  "keep_alive_ack",
370
378
  "mtk_update_complete",
@@ -571,6 +579,10 @@ class BluetoothSdkModule : Module() {
571
579
  requireSdk().setHotspotState(enabled).values
572
580
  }
573
581
 
582
+ SdkAsyncFunction("setWifiAdbState") { enabled: Boolean ->
583
+ sdk?.setWifiAdbState(enabled)
584
+ }
585
+
574
586
  SdkAsyncFunction("setSystemTime") { timestampMs: Double ->
575
587
  sdk?.setSystemTime(timestampMs.toLong())
576
588
  }
@@ -756,6 +768,10 @@ class BluetoothSdkModule : Module() {
756
768
  requireSdk().stopVideoRecording(requestId, webhookUrl, authToken).values
757
769
  }
758
770
 
771
+ SdkCoroutineFunction("queryVideoRecordingStatus") { requestId: String ->
772
+ requireSdk().queryVideoRecordingStatus(requestId).values
773
+ }
774
+
759
775
  // MARK: - Stream Commands
760
776
 
761
777
  SdkCoroutineFunction("startStream") { params: Map<String, Any> ->
@@ -145,6 +145,27 @@ public class Bridge private constructor() {
145
145
  sendTypedMessage("pair_failure", data as Map<String, Any>)
146
146
  }
147
147
 
148
+ @JvmStatic
149
+ fun sendPairingInfo(
150
+ hadPreviousBond: Boolean,
151
+ transferId: String? = null,
152
+ pairingCode: String? = null,
153
+ classicBondReady: Boolean = false,
154
+ securePairingCapable: Boolean = true,
155
+ protocolVersion: Int = 1,
156
+ binding: String? = null,
157
+ ) {
158
+ val data = HashMap<String, Any>()
159
+ data["had_previous_bond"] = hadPreviousBond
160
+ if (transferId != null) data["transfer_id"] = transferId
161
+ if (pairingCode != null) data["pairing_code"] = pairingCode
162
+ data["classic_bond_ready"] = classicBondReady
163
+ data["secure_pairing_capable"] = securePairingCapable
164
+ data["protocol_version"] = protocolVersion
165
+ if (binding != null) data["binding"] = binding
166
+ sendTypedMessage("pairing_info", data as Map<String, Any>)
167
+ }
168
+
148
169
  /** Send audio connected event - matches iOS implementation for platform parity */
149
170
  @JvmStatic
150
171
  fun sendAudioConnected(deviceName: String) {
@@ -172,6 +193,14 @@ public class Bridge private constructor() {
172
193
  sendTypedMessage("mic_lc3", body as Map<String, Any>)
173
194
  }
174
195
 
196
+ @JvmStatic
197
+ internal fun sendMicHealth(health: MicHealth, reason: String) {
198
+ val body = HashMap(health.toMap())
199
+ body["reason"] = reason
200
+ body["timestamp"] = System.currentTimeMillis()
201
+ sendTypedMessage("mic_health", body)
202
+ }
203
+
175
204
  private fun micPcmEventBody(data: ByteArray): HashMap<String, Any> {
176
205
  val voiceActivityDetectionEnabled =
177
206
  DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean
@@ -251,7 +280,10 @@ public class Bridge private constructor() {
251
280
  deviceName: String,
252
281
  deviceAddress: String = "",
253
282
  rssi: Int? = null,
254
- projectName: String? = null
283
+ projectName: String? = null,
284
+ pairingMode: Boolean? = null,
285
+ pairingCode: String? = null,
286
+ securePairingCapable: Boolean? = null,
255
287
  ) {
256
288
  val searchResults =
257
289
  (DeviceStore.store.getCategory("bluetooth")["searchResults"] as? List<*>)
@@ -274,6 +306,9 @@ public class Bridge private constructor() {
274
306
  }
275
307
  projectName?.takeIf { it.isNotBlank() }?.let { put("projectName", it) }
276
308
  rssi?.let { put("rssi", it) }
309
+ pairingMode?.let { put("pairingMode", it) }
310
+ pairingCode?.takeIf { it.isNotBlank() }?.let { put("pairingCode", it) }
311
+ securePairingCapable?.let { put("securePairingCapable", it) }
277
312
  }
278
313
  // Keep the public searchResults array stable as glasses are added or removed.
279
314
  // Duplicate discoveries refresh their existing row; only new glasses append.
@@ -462,6 +497,9 @@ public class Bridge private constructor() {
462
497
  }
463
498
  body["otaVersionUrl"] = stringField("otaVersionUrl", "ota_version_url")
464
499
  body["appVersion"] = stringField("appVersion", "app_version")
500
+ (values["hotspotOtaVersion"] as? Number
501
+ ?: values["hotspot_ota_version"] as? Number)
502
+ ?.let { body["hotspotOtaVersion"] = it.toInt() }
465
503
  sendTypedMessage("version_info", body)
466
504
  }
467
505
 
@@ -875,5 +913,3 @@ public class Bridge private constructor() {
875
913
  }
876
914
  }
877
915
 
878
-
879
-