@mentra/bluetooth-sdk 0.1.6 → 0.1.8

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 (110) hide show
  1. package/README.md +26 -22
  2. package/android/.project +28 -0
  3. package/android/build.gradle +5 -9
  4. package/android/lc3Lib/.project +28 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +54 -33
  6. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +48 -10
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +27 -36
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -4
  9. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +170 -14
  10. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -6
  11. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +69 -10
  12. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +5 -3
  13. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +2 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +149 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +13 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +297 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +120 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +13 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +1 -1
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3 -1
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1 -1
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +269 -28
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -3
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +4 -1
  26. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -12
  27. package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +4 -2
  28. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +131 -6
  29. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -3
  30. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +105 -0
  31. package/build/BluetoothSdk.types.d.ts +107 -48
  32. package/build/BluetoothSdk.types.d.ts.map +1 -1
  33. package/build/BluetoothSdk.types.js +4 -0
  34. package/build/BluetoothSdk.types.js.map +1 -1
  35. package/build/_private/BluetoothSdkModule.d.ts +8 -6
  36. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  37. package/build/_private/BluetoothSdkModule.js +58 -9
  38. package/build/_private/BluetoothSdkModule.js.map +1 -1
  39. package/build/index.d.ts +2 -2
  40. package/build/index.d.ts.map +1 -1
  41. package/build/index.js +6 -5
  42. package/build/index.js.map +1 -1
  43. package/build/photo-receiver/MentraPhotoReceiver.types.d.ts +18 -0
  44. package/build/photo-receiver/MentraPhotoReceiver.types.d.ts.map +1 -0
  45. package/build/photo-receiver/MentraPhotoReceiver.types.js +2 -0
  46. package/build/photo-receiver/MentraPhotoReceiver.types.js.map +1 -0
  47. package/build/photo-receiver/MentraPhotoReceiverModule.d.ts +10 -0
  48. package/build/photo-receiver/MentraPhotoReceiverModule.d.ts.map +1 -0
  49. package/build/photo-receiver/MentraPhotoReceiverModule.js +3 -0
  50. package/build/photo-receiver/MentraPhotoReceiverModule.js.map +1 -0
  51. package/build/photo-receiver/MentraPhotoReceiverModule.web.d.ts +10 -0
  52. package/build/photo-receiver/MentraPhotoReceiverModule.web.d.ts.map +1 -0
  53. package/build/photo-receiver/MentraPhotoReceiverModule.web.js +12 -0
  54. package/build/photo-receiver/MentraPhotoReceiverModule.web.js.map +1 -0
  55. package/build/photo-receiver/index.d.ts +3 -0
  56. package/build/photo-receiver/index.d.ts.map +1 -0
  57. package/build/photo-receiver/index.js +3 -0
  58. package/build/photo-receiver/index.js.map +1 -0
  59. package/build/react/useMentraBluetooth.d.ts +5 -3
  60. package/build/react/useMentraBluetooth.d.ts.map +1 -1
  61. package/build/react/useMentraBluetooth.js +15 -4
  62. package/build/react/useMentraBluetooth.js.map +1 -1
  63. package/expo-module.config.json +5 -2
  64. package/ios/BluetoothSdkModule.swift +59 -26
  65. package/ios/LocalPhotoUploadServer.swift +411 -0
  66. package/ios/MentraBluetoothSDK.podspec +8 -5
  67. package/ios/MentraPhotoReceiverModule.swift +147 -0
  68. package/ios/Source/Audio/AudioModels.swift +10 -10
  69. package/ios/Source/Bridge.swift +42 -12
  70. package/ios/Source/Camera/CameraModels.swift +74 -19
  71. package/ios/Source/DeviceManager.swift +23 -98
  72. package/ios/Source/DeviceStore.swift +12 -4
  73. package/ios/Source/Events/BluetoothEvents.swift +37 -0
  74. package/ios/Source/MentraBluetoothSDK.swift +135 -14
  75. package/ios/Source/Status/DeviceStatus.swift +12 -4
  76. package/ios/Source/Status/RuntimeState.swift +22 -13
  77. package/ios/Source/Streaming/StreamModels.swift +239 -45
  78. package/ios/Source/controllers/ControllerManager.swift +3 -3
  79. package/ios/Source/controllers/R1.swift +9 -3
  80. package/ios/Source/sgcs/G1.swift +5 -4
  81. package/ios/Source/sgcs/G2.swift +5 -6
  82. package/ios/Source/sgcs/Mach1.swift +1 -1
  83. package/ios/Source/sgcs/MentraLive.swift +183 -22
  84. package/ios/Source/sgcs/MentraNex.swift +1 -1
  85. package/ios/Source/sgcs/SGCManager.swift +11 -3
  86. package/ios/Source/sgcs/Simulated.swift +2 -2
  87. package/ios/Source/stt/SherpaOnnxTranscriber.swift +1 -4
  88. package/ios/Source/utils/MessageChunkReassembler.swift +89 -0
  89. package/package.json +6 -1
  90. package/src/BluetoothSdk.types.ts +133 -62
  91. package/src/_private/BluetoothSdkModule.ts +79 -26
  92. package/src/index.ts +15 -8
  93. package/src/photo-receiver/MentraPhotoReceiver.types.ts +20 -0
  94. package/src/photo-receiver/MentraPhotoReceiverModule.ts +14 -0
  95. package/src/photo-receiver/MentraPhotoReceiverModule.web.ts +20 -0
  96. package/src/photo-receiver/index.ts +2 -0
  97. package/src/react/useMentraBluetooth.ts +19 -7
  98. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +0 -221
  99. package/ios/Packages/VAD/Common/VAD/VADQuality.swift +0 -41
  100. package/ios/Packages/VAD/Common/VAD/VADState.swift +0 -26
  101. package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +0 -29
  102. package/ios/Packages/VAD/Common/VAD/VADType.swift +0 -14
  103. package/ios/Packages/VAD/Data/Configuration.swift +0 -68
  104. package/ios/Packages/VAD/Data/FrameSize.swift +0 -39
  105. package/ios/Packages/VAD/Data/Record.swift +0 -13
  106. package/ios/Packages/VAD/Data/Result.swift +0 -22
  107. package/ios/Packages/VAD/Data/SampleRate.swift +0 -48
  108. package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
  109. package/ios/Packages/VAD/Silero/SileroVAD.swift +0 -284
  110. package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +0 -64
package/README.md CHANGED
@@ -235,8 +235,8 @@ await BluetoothSdk.sendWifiCredentials('Office WiFi', 'secret')
235
235
  await BluetoothSdk.forgetWifiNetwork('Office WiFi')
236
236
  await BluetoothSdk.setHotspotState(true)
237
237
 
238
- await BluetoothSdk.setGalleryMode('auto')
239
- await BluetoothSdk.setGalleryMode('manual')
238
+ await BluetoothSdk.setGalleryModeEnabled(true)
239
+ await BluetoothSdk.setGalleryModeEnabled(false)
240
240
 
241
241
  await BluetoothSdk.setPreferredMic('auto')
242
242
  await BluetoothSdk.setMicState(true)
@@ -253,24 +253,30 @@ await BluetoothSdk.rgbLedControl(
253
253
  )
254
254
  ```
255
255
 
256
- `setMicState(true)` defaults to continuous microphone PCM from the glasses. VAD means Voice Activity Detection; pass `false` as the third argument only when your app intentionally wants VAD-gated microphone events.
256
+ `setMicState(true)` defaults to continuous microphone PCM from the glasses. The SDK does not apply phone-side Voice Activity Detection gating to microphone audio events. Use `setVoiceActivityDetectionEnabled(false)` when you want glasses-side Voice Activity Detection disabled for continuous external STT, recording, or playback. `voice_activity_detection_status` reports whether glasses-side Voice Activity Detection is enabled, and `speaking_status` reports speaking/not-speaking when supported. Microphone events include the latest `voiceActivityDetectionEnabled` value.
257
257
 
258
258
  ## Photo Upload
259
259
 
260
260
  ```ts
261
- await BluetoothSdk.requestPhoto(
262
- `photo-${Date.now()}`,
263
- 'com.example.app',
264
- 'medium',
265
- 'https://api.example.com/mentra/photo',
266
- 'optional-token',
267
- 'medium',
268
- true,
269
- )
261
+ await BluetoothSdk.requestPhoto({
262
+ requestId: `photo-${Date.now()}`,
263
+ appId: 'com.example.app',
264
+ size: 'medium',
265
+ webhookUrl: 'https://api.example.com/mentra/photo',
266
+ authToken: 'optional-token',
267
+ compress: 'medium',
268
+ sound: true,
269
+ exposureTimeNs: null, // auto exposure; pass a positive nanosecond value for manual exposure
270
+ iso: null, // auto ISO; pass a positive ISO only with manual exposureTimeNs
271
+ })
270
272
  ```
271
273
 
272
274
  The webhook should accept multipart form data with a `photo` file and `requestId`. If `authToken` is provided, the uploader adds `Authorization: Bearer <token>`. The camera light is always enabled for photo capture.
273
275
 
276
+ For one-shot manual capture tuning, pass `exposureTimeNs` and `iso` together. `exposureTimeNs` is sensor exposure time in nanoseconds; `iso` is sensor ISO. If `exposureTimeNs` is omitted, `null`, invalid, or unsupported by the connected glasses, the camera uses auto exposure and ignores `iso`.
277
+
278
+ Use `setCameraFov({fov, roiPosition})` to configure Mentra Live camera field of view and crop position. FOV is clamped to 82-118 degrees; ROI position is `0` center, `1` bottom, or `2` top. Applying FOV/ROI restarts the camera for about 5 seconds, so wait before requesting the next photo.
279
+
274
280
  ## Streaming
275
281
 
276
282
  ```ts
@@ -280,18 +286,14 @@ await BluetoothSdk.startStream({
280
286
  type: 'start_stream',
281
287
  streamUrl: 'http://192.168.1.42:8889/mentra-live/whip',
282
288
  streamId,
283
- })
284
-
285
- await BluetoothSdk.keepStreamAlive({
286
- type: 'keep_stream_alive',
287
- streamId,
288
- ackId: `ack-${Date.now()}`,
289
+ video: {fps: 15},
289
290
  })
290
291
 
291
292
  await BluetoothSdk.stopStream()
292
293
  ```
293
294
 
294
- Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https://` for WHIP/WebRTC ingest. Send keep-alives about every 15 seconds while streaming. The camera light is always enabled while streaming.
295
+ Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https://` for WHIP/WebRTC ingest. The SDK sends stream keep-alives automatically while streaming and reports keep-alive failures through `stream_status`. The camera light is always enabled while streaming.
296
+ `stream_status` events may include `resolvedConfig`, which reports the effective transport, video, and audio settings after glasses defaults, clamps, and camera preflight.
295
297
 
296
298
  ## Events
297
299
 
@@ -303,8 +305,10 @@ import {useBluetoothEvent} from '@mentra/bluetooth-sdk/react'
303
305
  export function HardwareEventLogger() {
304
306
  useBluetoothEvent('button_press', (event) => console.log(event))
305
307
  useBluetoothEvent('touch_event', (event) => console.log(event))
308
+ useBluetoothEvent('photo_status', (event) => console.log(event.status, event.resolvedConfig))
306
309
  useBluetoothEvent('photo_response', (event) => console.log(event))
307
310
  useBluetoothEvent('stream_status', (event) => console.log(event))
311
+ useBluetoothEvent('speaking_status', (event) => console.log(event.speaking))
308
312
  useBluetoothEvent('mic_pcm', (event) => {
309
313
  console.log(event.sampleRate, event.bitsPerSample, event.channels, event.encoding)
310
314
  console.log(event.pcm)
@@ -316,9 +320,9 @@ export function HardwareEventLogger() {
316
320
 
317
321
  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.
318
322
 
319
- Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `hotspot_status_change`, `photo_response`, `gallery_status`, `stream_status`, `keep_alive_ack`, `mic_pcm`, `mic_lc3`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
323
+ Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `hotspot_status_change`, `photo_status`, `photo_response`, `gallery_status`, `stream_status`, `mic_pcm`, `mic_lc3`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
320
324
 
321
- React Native event payload fields use camelCase. For example, `touch_event` includes `gestureName`, `photo_response` success includes `uploadUrl`, and `gallery_status` includes `hasContent` and `cameraBusy`. `mic_pcm` includes `sampleRate`, `bitsPerSample`, `channels`, `encoding`, and `vadGated`; `mic_lc3` includes `sampleRate`, `channels`, `encoding`, `frameDurationMs`, `frameSizeBytes`, `bitrate`, `packetizedFromGlasses`, and `vadGated`.
325
+ React Native event payload fields use camelCase. For example, `touch_event` includes `gestureName`, `photo_response` success includes `uploadUrl`, and `gallery_status` includes `hasContent` and `cameraBusy`. `photo_status` reports intermediate photo states such as `accepted`, `queued`, `configuring`, `capturing`, `captured`, `compressing`, `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. `mic_pcm` includes `sampleRate`, `bitsPerSample`, `channels`, and `encoding`; `mic_lc3` includes `sampleRate`, `channels`, `encoding`, `frameDurationMs`, `frameSizeBytes`, `bitrate`, and `packetizedFromGlasses`.
322
326
 
323
327
  Only documented imports are supported for app developers. Undocumented package subpaths or symbols with a leading underscore can change without notice.
324
328
 
@@ -335,4 +339,4 @@ Use `bunx expo run:android` for Android. Keep local paths in your shell or CI en
335
339
 
336
340
  ## Starter Example App
337
341
 
338
- The [Mentra Bluetooth SDK Starter Kit](https://github.com/Mentra-Community/Mentra-Bluetooth-SDK-Starter-Kit) includes starter example apps for Android, iOS, and React Native / Expo. The React Native starter demonstrates scan/connect, display, camera photo upload, RTMP/SRT/WebRTC streaming, Wi-Fi/hotspot, microphone PCM, RGB LED, gallery-button mode, and console event inspection.
342
+ The [Mentra Bluetooth SDK Starter Kit](https://github.com/Mentra-Community/Mentra-Bluetooth-SDK-Starter-Kit) includes starter example apps for Android, iOS, and React Native / Expo. The React Native starter demonstrates scan/connect, display, camera photo upload, RTMP/SRT/WebRTC streaming, Wi-Fi/hotspot, microphone PCM, RGB LED, gallery mode, and console event inspection.
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <projectDescription>
3
+ <name>mentra-bluetooth-sdk</name>
4
+ <comment>Project mentra-bluetooth-sdk created by Buildship.</comment>
5
+ <projects>
6
+ </projects>
7
+ <buildSpec>
8
+ <buildCommand>
9
+ <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
10
+ <arguments>
11
+ </arguments>
12
+ </buildCommand>
13
+ </buildSpec>
14
+ <natures>
15
+ <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
16
+ </natures>
17
+ <filteredResources>
18
+ <filter>
19
+ <id>1779400133450</id>
20
+ <name></name>
21
+ <type>30</type>
22
+ <matcher>
23
+ <id>org.eclipse.core.resources.regexFilterMatcher</id>
24
+ <arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
25
+ </matcher>
26
+ </filter>
27
+ </filteredResources>
28
+ </projectDescription>
@@ -83,19 +83,15 @@ dependencies {
83
83
  // OkHttp for HTTP requests
84
84
  implementation 'com.squareup.okhttp3:okhttp:4.12.0'
85
85
 
86
+ // Local photo upload receiver
87
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
88
+
86
89
  // LC3 audio codec library
87
90
  implementation project(':lc3Lib')
88
91
 
89
92
  // Sherpa-ONNX for local transcription
90
93
  implementation 'com.github.k2-fsa:sherpa-onnx:v1.12.8'
91
94
 
92
- // Silero VAD (with onnxruntime exclusions to avoid conflicts with sherpa-onnx)
93
- implementation ('com.github.gkonovalov:android-vad:2.0.9') {
94
- exclude group: 'com.microsoft.onnxruntime', module: 'onnxruntime'
95
- exclude group: 'com.microsoft.onnxruntime', module: 'onnxruntime4j'
96
- exclude group: 'com.microsoft.onnxruntime', module: 'onnxruntime4j-jni'
97
- }
98
-
99
95
  // Vuzix Z100
100
96
  implementation 'com.vuzix:ultralite-sdk-android:1.8'
101
97
  implementation 'com.squareup.picasso:picasso:2.8'
@@ -110,11 +106,11 @@ configurations.all {
110
106
  eachDependency { DependencyResolveDetails details ->
111
107
  if (details.requested.group == 'com.microsoft.onnxruntime') {
112
108
  details.useVersion '1.17.1'
113
- details.because('Force override vad\'s transitive version')
109
+ details.because('Force override transitive onnxruntime version')
114
110
  }
115
111
  if (details.requested.name == 'onnxruntime-android') {
116
112
  details.useVersion '1.17.1'
117
- details.because('Force override vad\'s transitive version')
113
+ details.because('Force override transitive onnxruntime version')
118
114
  }
119
115
  }
120
116
  }
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <projectDescription>
3
+ <name>lc3Lib</name>
4
+ <comment>Project lc3Lib created by Buildship.</comment>
5
+ <projects>
6
+ </projects>
7
+ <buildSpec>
8
+ <buildCommand>
9
+ <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
10
+ <arguments>
11
+ </arguments>
12
+ </buildCommand>
13
+ </buildSpec>
14
+ <natures>
15
+ <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
16
+ </natures>
17
+ <filteredResources>
18
+ <filter>
19
+ <id>1779400133449</id>
20
+ <name></name>
21
+ <type>30</type>
22
+ <matcher>
23
+ <id>org.eclipse.core.resources.regexFilterMatcher</id>
24
+ <arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
25
+ </matcher>
26
+ </filter>
27
+ </filteredResources>
28
+ </projectDescription>
@@ -1,5 +1,6 @@
1
1
  package com.mentra.bluetoothsdk
2
2
 
3
+ import com.mentra.bluetoothsdk.debug.BleTraceLogger
3
4
  import com.mentra.bluetoothsdk.utils.DeviceTypes
4
5
  import expo.modules.kotlin.modules.Module
5
6
  import expo.modules.kotlin.modules.ModuleDefinition
@@ -21,7 +22,10 @@ class BluetoothSdkModule : Module() {
21
22
  sendEvent(
22
23
  "bluetooth_status",
23
24
  sdk?.getRawBluetoothStatus()?.toMap()
24
- ?: DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
25
+ ?: BluetoothStatus.fromMap(
26
+ DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
27
+ )
28
+ .toMap()
25
29
  )
26
30
  }
27
31
 
@@ -73,6 +77,14 @@ class BluetoothSdkModule : Module() {
73
77
  sendEvent("head_up", mapOf("up" to headUp))
74
78
  }
75
79
 
80
+ override fun onVoiceActivityDetectionStatus(event: VoiceActivityDetectionStatusEvent) {
81
+ sendEvent("voice_activity_detection_status", event.values)
82
+ }
83
+
84
+ override fun onSpeakingStatus(event: SpeakingStatusEvent) {
85
+ sendEvent("speaking_status", event.values)
86
+ }
87
+
76
88
  override fun onBatteryStatus(event: BatteryStatusEvent) {
77
89
  sendEvent("battery_status", event.values)
78
90
  }
@@ -97,6 +109,10 @@ class BluetoothSdkModule : Module() {
97
109
  sendEvent("photo_response", event.values)
98
110
  }
99
111
 
112
+ override fun onPhotoStatus(event: PhotoStatusEvent) {
113
+ sendEvent("photo_status", event.values)
114
+ }
115
+
100
116
  override fun onStreamStatus(event: StreamStatusEvent) {
101
117
  sendEvent("stream_status", event.values)
102
118
  }
@@ -145,13 +161,15 @@ class BluetoothSdkModule : Module() {
145
161
  "button_press",
146
162
  "touch_event",
147
163
  "head_up",
148
- "vad_status",
164
+ "voice_activity_detection_status",
165
+ "speaking_status",
149
166
  "battery_status",
150
167
  "local_transcription",
151
168
  "wifi_status_change",
152
169
  "hotspot_status_change",
153
170
  "hotspot_error",
154
171
  "photo_response",
172
+ "photo_status",
155
173
  "gallery_status",
156
174
  "compatible_glasses_search_stop",
157
175
  "heartbeat_sent",
@@ -191,11 +209,17 @@ class BluetoothSdkModule : Module() {
191
209
  appContext.reactContext
192
210
  ?: appContext.currentActivity
193
211
  ?: throw IllegalStateException("No context available")
212
+ BleTraceLogger.logLifecycle(context, "BluetoothSdkModule", "module_create")
194
213
  sdk = MentraBluetoothSdk.create(context, sdkListener)
195
214
  deviceManager = DeviceManager.getInstance()
196
215
  }
197
216
 
198
217
  OnDestroy {
218
+ BleTraceLogger.logLifecycle(
219
+ appContext.reactContext ?: appContext.currentActivity,
220
+ "BluetoothSdkModule",
221
+ "module_destroy"
222
+ )
199
223
  sdk?.close()
200
224
  sdk = null
201
225
  deviceManager = null
@@ -209,7 +233,9 @@ class BluetoothSdkModule : Module() {
209
233
  }
210
234
 
211
235
  Function("getBluetoothStatus") {
212
- sdk?.getRawBluetoothStatus()?.toMap() ?: DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
236
+ sdk?.getRawBluetoothStatus()?.toMap()
237
+ ?: BluetoothStatus.fromMap(DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY))
238
+ .toMap()
213
239
  }
214
240
 
215
241
  Function("getDefaultDevice") { sdk?.getDefaultDevice()?.toMap() }
@@ -344,37 +370,34 @@ class BluetoothSdkModule : Module() {
344
370
 
345
371
  // MARK: - Gallery Commands
346
372
 
347
- AsyncFunction("setGalleryMode") { mode: String ->
348
- val galleryMode =
349
- when (mode.lowercase()) {
350
- "auto" -> GalleryMode.AUTO
351
- "manual" -> GalleryMode.MANUAL
352
- else -> throw IllegalArgumentException("setGalleryMode mode must be \"auto\" or \"manual\".")
353
- }
354
- sdk?.setGalleryMode(galleryMode)
373
+ AsyncFunction("setGalleryModeEnabled") { enabled: Boolean ->
374
+ sdk?.setGalleryModeEnabled(enabled)
375
+ }
376
+
377
+ AsyncFunction("setVoiceActivityDetectionEnabled") { enabled: Boolean ->
378
+ sdk?.setVoiceActivityDetectionEnabled(enabled)
355
379
  }
356
380
 
357
381
  AsyncFunction("queryGalleryStatus") { sdk?.queryGalleryStatus() }
358
382
 
359
- AsyncFunction("requestPhoto") {
360
- requestId: String,
361
- appId: String,
362
- size: String,
363
- webhookUrl: String,
364
- authToken: String?,
365
- compress: String,
366
- sound: Boolean ->
367
- sdk?.requestPhoto(
368
- PhotoRequest(
369
- requestId = requestId,
370
- appId = appId,
371
- size = PhotoSize.fromValue(size),
372
- webhookUrl = webhookUrl,
373
- authToken = authToken,
374
- compress = PhotoCompression.fromValue(compress),
375
- sound = sound,
376
- )
383
+ AsyncFunction("requestPhoto") { params: Map<String, Any?> ->
384
+ // JS may pass null for optional fields; Map<String, Any> rejects null values at the bridge.
385
+ val sanitized =
386
+ params.mapNotNull { (key, value) ->
387
+ if (value == null) null else key to value
388
+ }.toMap()
389
+ val req = PhotoRequest.fromMap(sanitized)
390
+ Bridge.log(
391
+ "NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} appId=${req.appId} size=${req.size} compress=${req.compress} flash=${req.flash} sound=${req.sound} exposureTimeNs=${req.exposureTimeNs} iso=${req.iso}"
377
392
  )
393
+ val activeSdk = sdk
394
+ if (activeSdk == null) {
395
+ Bridge.log(
396
+ "NATIVE: PHOTO PIPELINE — sdk is null; requestPhoto dropped requestId=${req.requestId}"
397
+ )
398
+ } else {
399
+ activeSdk.requestPhoto(req)
400
+ }
378
401
  }
379
402
 
380
403
  // MARK: - OTA Commands
@@ -411,8 +434,8 @@ class BluetoothSdkModule : Module() {
411
434
 
412
435
  AsyncFunction("stopStream") { sdk?.stopStream() }
413
436
 
414
- AsyncFunction("keepStreamAlive") { params: Map<String, Any> ->
415
- sdk?.keepStreamAlive(StreamKeepAliveRequest.fromMap(params))
437
+ AsyncFunction("sendCloudStreamKeepAlive") { params: Map<String, Any> ->
438
+ sdk?.sendCloudStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
416
439
  }
417
440
 
418
441
  // MARK: - Microphone Commands
@@ -420,13 +443,11 @@ class BluetoothSdkModule : Module() {
420
443
  AsyncFunction("setMicState") {
421
444
  enabled: Boolean,
422
445
  useGlassesMic: Boolean?,
423
- bypassVad: Boolean?,
424
446
  sendTranscript: Boolean?,
425
447
  sendLc3Data: Boolean? ->
426
448
  sdk?.setMicState(
427
449
  enabled = enabled,
428
450
  useGlassesMic = useGlassesMic ?: true,
429
- bypassVad = bypassVad ?: true,
430
451
  sendTranscript = sendTranscript ?: false,
431
452
  sendLc3Data = sendLc3Data ?: false,
432
453
  )
@@ -9,6 +9,7 @@ package com.mentra.bluetoothsdk
9
9
 
10
10
  import android.util.Base64
11
11
  import android.util.Log
12
+ import com.mentra.bluetoothsdk.debug.BleTraceLogger
12
13
  import java.util.HashMap
13
14
  import java.util.UUID
14
15
  import kotlin.jvm.JvmStatic
@@ -150,17 +151,23 @@ public class Bridge private constructor() {
150
151
  }
151
152
 
152
153
  private fun micPcmEventBody(data: ByteArray): HashMap<String, Any> {
154
+ val voiceActivityDetectionEnabled =
155
+ DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean
156
+ ?: true
153
157
  val body = HashMap<String, Any>()
154
158
  body["pcm"] = data
155
159
  body["sampleRate"] = MIC_SAMPLE_RATE
156
160
  body["bitsPerSample"] = PCM_BITS_PER_SAMPLE
157
161
  body["channels"] = MIC_CHANNELS
158
162
  body["encoding"] = "pcm_s16le"
159
- body["vadGated"] = isVadGated()
163
+ body["voiceActivityDetectionEnabled"] = voiceActivityDetectionEnabled
160
164
  return body
161
165
  }
162
166
 
163
167
  private fun micLc3EventBody(data: ByteArray): HashMap<String, Any> {
168
+ val voiceActivityDetectionEnabled =
169
+ DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean
170
+ ?: true
164
171
  val frameSizeBytes =
165
172
  (DeviceStore.store.get("bluetooth", "lc3_frame_size") as? Number)?.toInt()
166
173
  ?: DEFAULT_LC3_FRAME_SIZE_BYTES
@@ -173,13 +180,10 @@ public class Bridge private constructor() {
173
180
  body["frameSizeBytes"] = frameSizeBytes
174
181
  body["bitrate"] = frameSizeBytes * 8 * (1000 / LC3_FRAME_DURATION_MS)
175
182
  body["packetizedFromGlasses"] = false
176
- body["vadGated"] = isVadGated()
183
+ body["voiceActivityDetectionEnabled"] = voiceActivityDetectionEnabled
177
184
  return body
178
185
  }
179
186
 
180
- private fun isVadGated(): Boolean =
181
- !((DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean) ?: true)
182
-
183
187
  /** Save a setting */
184
188
  @JvmStatic
185
189
  fun saveSetting(key: String, value: Any) {
@@ -189,12 +193,22 @@ public class Bridge private constructor() {
189
193
  sendTypedMessage("save_setting", body as Map<String, Any>)
190
194
  }
191
195
 
192
- /** Send VAD (Voice Activity Detection) status */
196
+ /** Send Voice Activity Detection status */
193
197
  @JvmStatic
194
- fun sendVadEvent(isSpeaking: Boolean) {
198
+ fun sendVoiceActivityDetectionStatus(enabled: Boolean) {
199
+ DeviceStore.set("glasses", "voiceActivityDetectionEnabled", enabled)
195
200
  val body = HashMap<String, Any>()
196
- body["status"] = isSpeaking
197
- sendTypedMessage("vad_status", body as Map<String, Any>)
201
+ body["voiceActivityDetectionEnabled"] = enabled
202
+ sendTypedMessage("voice_activity_detection_status", body as Map<String, Any>)
203
+ }
204
+
205
+ /** Send live speaking status reported by glasses-side Voice Activity Detection. */
206
+ @JvmStatic
207
+ fun sendSpeakingStatus(speaking: Boolean) {
208
+ val body = HashMap<String, Any>()
209
+ body["speaking"] = speaking
210
+ body["timestamp"] = System.currentTimeMillis()
211
+ sendTypedMessage("speaking_status", body as Map<String, Any>)
198
212
  }
199
213
 
200
214
  /** Send battery status */
@@ -334,14 +348,29 @@ public class Bridge private constructor() {
334
348
 
335
349
  @JvmStatic
336
350
  fun sendPhotoError(requestId: String, errorCode: String, errorMessage: String) {
351
+ val timestamp = System.currentTimeMillis()
337
352
  val event = HashMap<String, Any>()
338
353
  event["type"] = "photo_response"
339
354
  event["state"] = "error"
340
355
  event["requestId"] = requestId
341
356
  event["errorCode"] = errorCode
342
357
  event["errorMessage"] = errorMessage
343
- event["timestamp"] = System.currentTimeMillis()
358
+ event["timestamp"] = timestamp
344
359
  sendTypedMessage("photo_response", event as Map<String, Any>)
360
+
361
+ val status = HashMap<String, Any>()
362
+ status["type"] = "photo_status"
363
+ status["status"] = "failed"
364
+ status["requestId"] = requestId
365
+ status["errorCode"] = errorCode
366
+ status["errorMessage"] = errorMessage
367
+ status["timestamp"] = timestamp
368
+ sendTypedMessage("photo_status", status as Map<String, Any>)
369
+ }
370
+
371
+ @JvmStatic
372
+ fun sendPhotoStatus(statusJson: Map<String, Any>) {
373
+ sendTypedMessage("photo_status", statusJson)
345
374
  }
346
375
 
347
376
  /** Send RGB LED control response */
@@ -632,6 +661,15 @@ public class Bridge private constructor() {
632
661
  return
633
662
  }
634
663
 
664
+ if (type != "log") {
665
+ BleTraceLogger.logMap(
666
+ "phone_to_app",
667
+ "sdk_event_dispatch",
668
+ type,
669
+ mutableBody as Map<String, Any>,
670
+ )
671
+ }
672
+
635
673
  // Send directly using type as event name - no JSON serialization
636
674
  sinks.forEach { sink ->
637
675
  try {
@@ -132,10 +132,6 @@ class DeviceManager {
132
132
  get() = DeviceStore.store.get("bluetooth", "power_saving_mode") as? Boolean ?: false
133
133
  set(value) = DeviceStore.apply("bluetooth", "power_saving_mode", value)
134
134
 
135
- private var bypassVad: Boolean
136
- get() = DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean ?: true
137
- set(value) = DeviceStore.apply("bluetooth", "bypass_vad", value)
138
-
139
135
  private var offlineCaptionsRunning: Boolean
140
136
  get() = DeviceStore.store.get("bluetooth", "offline_captions_running") as? Boolean ?: false
141
137
  set(value) = DeviceStore.apply("bluetooth", "offline_captions_running", value)
@@ -169,8 +165,8 @@ class DeviceManager {
169
165
  set(value) = DeviceStore.apply("bluetooth", "dashboard_depth", value)
170
166
 
171
167
  private var galleryMode: Boolean
172
- get() = DeviceStore.store.get("bluetooth", "galleryModeAuto") as? Boolean ?: true
173
- set(value) = DeviceStore.apply("bluetooth", "galleryModeAuto", value)
168
+ get() = DeviceStore.store.get("bluetooth", "gallery_mode") as? Boolean ?: true
169
+ set(value) = DeviceStore.apply("bluetooth", "gallery_mode", value)
174
170
 
175
171
  // state:
176
172
  private var searching: Boolean
@@ -241,10 +237,6 @@ class DeviceManager {
241
237
  private var lastLc3Event: Long? = null
242
238
  private var micReinitRunnable: Runnable? = null
243
239
 
244
- // VAD
245
- private val vadBuffer = mutableListOf<ByteArray>()
246
- private var isSpeaking = false
247
-
248
240
  // STT
249
241
  private var transcriber: SherpaOnnxTranscriber? = null
250
242
 
@@ -604,13 +596,6 @@ class DeviceManager {
604
596
 
605
597
  // MARK: - Voice Data Handling
606
598
 
607
- private fun checkSetVadStatus(speaking: Boolean) {
608
- if (speaking != isSpeaking) {
609
- isSpeaking = speaking
610
- Bridge.sendVadEvent(isSpeaking)
611
- }
612
- }
613
-
614
599
  private fun convertAndSendMicLc3(pcmData: ByteArray) {
615
600
  synchronized(lc3Lock) {
616
601
  if (lc3EncoderPtr == 0L) {
@@ -636,21 +621,6 @@ class DeviceManager {
636
621
  }
637
622
  }
638
623
 
639
- private fun emptyVadBuffer() {
640
- while (vadBuffer.isNotEmpty()) {
641
- val chunk = vadBuffer.removeAt(0)
642
- handleSendingPcm(chunk) // Uses our encoder, not Bridge directly
643
- }
644
- }
645
-
646
- private fun addToVadBuffer(chunk: ByteArray) {
647
- val MAX_BUFFER_SIZE = 20
648
- vadBuffer.add(chunk)
649
- while (vadBuffer.size > MAX_BUFFER_SIZE) {
650
- vadBuffer.removeAt(0)
651
- }
652
- }
653
-
654
624
  /**
655
625
  * Handle raw LC3 audio data from glasses. Decodes the glasses LC3, then passes to handlePcm for
656
626
  * canonical LC3 encoding. Note: frameSize here is for glasses→phone decoding, NOT for
@@ -1127,6 +1097,7 @@ class DeviceManager {
1127
1097
  fun handleDeviceDisconnected() {
1128
1098
  Bridge.log("MAN: Device disconnected")
1129
1099
  DeviceStore.apply("glasses", "headUp", false)
1100
+ DeviceStore.apply("glasses", "voiceActivityDetectionEnabled", true)
1130
1101
  }
1131
1102
 
1132
1103
  fun handleControllerReady() {
@@ -1369,7 +1340,6 @@ class DeviceManager {
1369
1340
  val willSendPcm = shouldSendPcm || shouldSendLc3
1370
1341
  val willSendTranscript = shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive
1371
1342
  micEnabled = willSendPcm || willSendTranscript
1372
- vadBuffer.clear()
1373
1343
  updateMicState()
1374
1344
  }
1375
1345
 
@@ -1380,10 +1350,30 @@ class DeviceManager {
1380
1350
  webhookUrl: String,
1381
1351
  authToken: String?,
1382
1352
  compress: String,
1383
- sound: Boolean
1353
+ flash: Boolean,
1354
+ sound: Boolean,
1355
+ exposureTimeNs: Double? = null,
1356
+ iso: Int? = null,
1384
1357
  ) {
1385
- Bridge.log("MAN: requestPhoto: $requestId, $appId, $size, compress=$compress, flash=true, sound=$sound")
1386
- sgc?.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, true, sound)
1358
+ val exposureNs: Long? =
1359
+ exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
1360
+ when {
1361
+ v > Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
1362
+ else -> v.toLong()
1363
+ }
1364
+ }
1365
+ val manualIso = if (exposureNs != null) iso?.takeIf { it > 0 } else null
1366
+ Bridge.log(
1367
+ "MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=$requestId appId=$appId size=$size compress=$compress flash=$flash sound=$sound exposureTimeNs=$exposureNs iso=${manualIso ?: "auto"} sgc=${sgc?.javaClass?.simpleName ?: "null"}"
1368
+ )
1369
+ val activeSgc = sgc
1370
+ if (activeSgc == null) {
1371
+ Bridge.log(
1372
+ "MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=$requestId"
1373
+ )
1374
+ return
1375
+ }
1376
+ activeSgc.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, sound, exposureNs, manualIso)
1387
1377
  }
1388
1378
 
1389
1379
  fun rgbLedControl(
@@ -1513,6 +1503,7 @@ class DeviceManager {
1513
1503
  DeviceStore.apply("glasses", "deviceModel", "")
1514
1504
  DeviceStore.apply("glasses", "fullyBooted", false)
1515
1505
  DeviceStore.apply("glasses", "connected", false)
1506
+ DeviceStore.apply("glasses", "voiceActivityDetectionEnabled", true)
1516
1507
  // disconnect the controller as well:
1517
1508
  searchingController = false
1518
1509
  DeviceStore.apply("glasses", "controllerConnected", false)