@mentra/bluetooth-sdk 0.1.6 → 0.1.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 (79) hide show
  1. package/README.md +16 -14
  2. package/android/build.gradle +2 -9
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +40 -31
  4. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +22 -9
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +25 -36
  6. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -4
  7. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -10
  8. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -6
  9. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +29 -7
  10. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +4 -3
  11. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +2 -1
  12. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +12 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +1 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +2 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1 -1
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +168 -10
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +7 -3
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +3 -1
  20. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -12
  21. package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +4 -2
  22. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +3 -3
  23. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -3
  24. package/build/BluetoothSdk.types.d.ts +42 -20
  25. package/build/BluetoothSdk.types.d.ts.map +1 -1
  26. package/build/BluetoothSdk.types.js +1 -0
  27. package/build/BluetoothSdk.types.js.map +1 -1
  28. package/build/_private/BluetoothSdkModule.d.ts +6 -5
  29. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  30. package/build/_private/BluetoothSdkModule.js +37 -5
  31. package/build/_private/BluetoothSdkModule.js.map +1 -1
  32. package/build/index.d.ts +1 -1
  33. package/build/index.d.ts.map +1 -1
  34. package/build/index.js +4 -2
  35. package/build/index.js.map +1 -1
  36. package/build/react/useMentraBluetooth.d.ts +5 -3
  37. package/build/react/useMentraBluetooth.d.ts.map +1 -1
  38. package/build/react/useMentraBluetooth.js +15 -4
  39. package/build/react/useMentraBluetooth.js.map +1 -1
  40. package/ios/BluetoothSdkModule.swift +41 -24
  41. package/ios/MentraBluetoothSDK.podspec +0 -3
  42. package/ios/Source/Audio/AudioModels.swift +10 -10
  43. package/ios/Source/Bridge.swift +22 -11
  44. package/ios/Source/Camera/CameraModels.swift +13 -11
  45. package/ios/Source/DeviceManager.swift +20 -97
  46. package/ios/Source/DeviceStore.swift +12 -4
  47. package/ios/Source/Events/BluetoothEvents.swift +34 -0
  48. package/ios/Source/MentraBluetoothSDK.swift +25 -10
  49. package/ios/Source/Status/DeviceStatus.swift +12 -4
  50. package/ios/Source/Status/RuntimeState.swift +22 -13
  51. package/ios/Source/Streaming/StreamModels.swift +5 -5
  52. package/ios/Source/controllers/ControllerManager.swift +3 -3
  53. package/ios/Source/controllers/R1.swift +1 -1
  54. package/ios/Source/sgcs/G1.swift +1 -1
  55. package/ios/Source/sgcs/G2.swift +2 -3
  56. package/ios/Source/sgcs/Mach1.swift +1 -1
  57. package/ios/Source/sgcs/MentraLive.swift +109 -12
  58. package/ios/Source/sgcs/MentraNex.swift +1 -1
  59. package/ios/Source/sgcs/SGCManager.swift +11 -3
  60. package/ios/Source/sgcs/Simulated.swift +2 -2
  61. package/ios/Source/stt/SherpaOnnxTranscriber.swift +1 -4
  62. package/package.json +1 -1
  63. package/src/BluetoothSdk.types.ts +44 -28
  64. package/src/_private/BluetoothSdkModule.ts +44 -21
  65. package/src/index.ts +6 -4
  66. package/src/react/useMentraBluetooth.ts +19 -7
  67. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +0 -221
  68. package/ios/Packages/VAD/Common/VAD/VADQuality.swift +0 -41
  69. package/ios/Packages/VAD/Common/VAD/VADState.swift +0 -26
  70. package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +0 -29
  71. package/ios/Packages/VAD/Common/VAD/VADType.swift +0 -14
  72. package/ios/Packages/VAD/Data/Configuration.swift +0 -68
  73. package/ios/Packages/VAD/Data/FrameSize.swift +0 -39
  74. package/ios/Packages/VAD/Data/Record.swift +0 -13
  75. package/ios/Packages/VAD/Data/Result.swift +0 -22
  76. package/ios/Packages/VAD/Data/SampleRate.swift +0 -48
  77. package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
  78. package/ios/Packages/VAD/Silero/SileroVAD.swift +0 -284
  79. 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,20 +253,20 @@ 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
+ })
270
270
  ```
271
271
 
272
272
  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.
@@ -280,6 +280,7 @@ await BluetoothSdk.startStream({
280
280
  type: 'start_stream',
281
281
  streamUrl: 'http://192.168.1.42:8889/mentra-live/whip',
282
282
  streamId,
283
+ video: {fps: 15},
283
284
  })
284
285
 
285
286
  await BluetoothSdk.keepStreamAlive({
@@ -305,6 +306,7 @@ export function HardwareEventLogger() {
305
306
  useBluetoothEvent('touch_event', (event) => console.log(event))
306
307
  useBluetoothEvent('photo_response', (event) => console.log(event))
307
308
  useBluetoothEvent('stream_status', (event) => console.log(event))
309
+ useBluetoothEvent('speaking_status', (event) => console.log(event.speaking))
308
310
  useBluetoothEvent('mic_pcm', (event) => {
309
311
  console.log(event.sampleRate, event.bitsPerSample, event.channels, event.encoding)
310
312
  console.log(event.pcm)
@@ -318,7 +320,7 @@ For non-React modules, `BluetoothSdk.addListener(...)` is the low-level subscrip
318
320
 
319
321
  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`.
320
322
 
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`.
323
+ 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`, and `encoding`; `mic_lc3` includes `sampleRate`, `channels`, `encoding`, `frameDurationMs`, `frameSizeBytes`, `bitrate`, and `packetizedFromGlasses`.
322
324
 
323
325
  Only documented imports are supported for app developers. Undocumented package subpaths or symbols with a leading underscore can change without notice.
324
326
 
@@ -335,4 +337,4 @@ Use `bunx expo run:android` for Android. Keep local paths in your shell or CI en
335
337
 
336
338
  ## Starter Example App
337
339
 
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.
340
+ 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.
@@ -89,13 +89,6 @@ dependencies {
89
89
  // Sherpa-ONNX for local transcription
90
90
  implementation 'com.github.k2-fsa:sherpa-onnx:v1.12.8'
91
91
 
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
92
  // Vuzix Z100
100
93
  implementation 'com.vuzix:ultralite-sdk-android:1.8'
101
94
  implementation 'com.squareup.picasso:picasso:2.8'
@@ -110,11 +103,11 @@ configurations.all {
110
103
  eachDependency { DependencyResolveDetails details ->
111
104
  if (details.requested.group == 'com.microsoft.onnxruntime') {
112
105
  details.useVersion '1.17.1'
113
- details.because('Force override vad\'s transitive version')
106
+ details.because('Force override transitive onnxruntime version')
114
107
  }
115
108
  if (details.requested.name == 'onnxruntime-android') {
116
109
  details.useVersion '1.17.1'
117
- details.because('Force override vad\'s transitive version')
110
+ details.because('Force override transitive onnxruntime version')
118
111
  }
119
112
  }
120
113
  }
@@ -21,7 +21,10 @@ class BluetoothSdkModule : Module() {
21
21
  sendEvent(
22
22
  "bluetooth_status",
23
23
  sdk?.getRawBluetoothStatus()?.toMap()
24
- ?: DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
24
+ ?: BluetoothStatus.fromMap(
25
+ DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
26
+ )
27
+ .toMap()
25
28
  )
26
29
  }
27
30
 
@@ -73,6 +76,14 @@ class BluetoothSdkModule : Module() {
73
76
  sendEvent("head_up", mapOf("up" to headUp))
74
77
  }
75
78
 
79
+ override fun onVoiceActivityDetectionStatus(event: VoiceActivityDetectionStatusEvent) {
80
+ sendEvent("voice_activity_detection_status", event.values)
81
+ }
82
+
83
+ override fun onSpeakingStatus(event: SpeakingStatusEvent) {
84
+ sendEvent("speaking_status", event.values)
85
+ }
86
+
76
87
  override fun onBatteryStatus(event: BatteryStatusEvent) {
77
88
  sendEvent("battery_status", event.values)
78
89
  }
@@ -145,7 +156,8 @@ class BluetoothSdkModule : Module() {
145
156
  "button_press",
146
157
  "touch_event",
147
158
  "head_up",
148
- "vad_status",
159
+ "voice_activity_detection_status",
160
+ "speaking_status",
149
161
  "battery_status",
150
162
  "local_transcription",
151
163
  "wifi_status_change",
@@ -209,7 +221,9 @@ class BluetoothSdkModule : Module() {
209
221
  }
210
222
 
211
223
  Function("getBluetoothStatus") {
212
- sdk?.getRawBluetoothStatus()?.toMap() ?: DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
224
+ sdk?.getRawBluetoothStatus()?.toMap()
225
+ ?: BluetoothStatus.fromMap(DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY))
226
+ .toMap()
213
227
  }
214
228
 
215
229
  Function("getDefaultDevice") { sdk?.getDefaultDevice()?.toMap() }
@@ -344,37 +358,34 @@ class BluetoothSdkModule : Module() {
344
358
 
345
359
  // MARK: - Gallery Commands
346
360
 
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)
361
+ AsyncFunction("setGalleryModeEnabled") { enabled: Boolean ->
362
+ sdk?.setGalleryModeEnabled(enabled)
363
+ }
364
+
365
+ AsyncFunction("setVoiceActivityDetectionEnabled") { enabled: Boolean ->
366
+ sdk?.setVoiceActivityDetectionEnabled(enabled)
355
367
  }
356
368
 
357
369
  AsyncFunction("queryGalleryStatus") { sdk?.queryGalleryStatus() }
358
370
 
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
- )
371
+ AsyncFunction("requestPhoto") { params: Map<String, Any?> ->
372
+ // JS may pass null for optional fields; Map<String, Any> rejects null values at the bridge.
373
+ val sanitized =
374
+ params.mapNotNull { (key, value) ->
375
+ if (value == null) null else key to value
376
+ }.toMap()
377
+ val req = PhotoRequest.fromMap(sanitized)
378
+ Bridge.log(
379
+ "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}"
377
380
  )
381
+ val activeSdk = sdk
382
+ if (activeSdk == null) {
383
+ Bridge.log(
384
+ "NATIVE: PHOTO PIPELINE — sdk is null; requestPhoto dropped requestId=${req.requestId}"
385
+ )
386
+ } else {
387
+ activeSdk.requestPhoto(req)
388
+ }
378
389
  }
379
390
 
380
391
  // MARK: - OTA Commands
@@ -420,13 +431,11 @@ class BluetoothSdkModule : Module() {
420
431
  AsyncFunction("setMicState") {
421
432
  enabled: Boolean,
422
433
  useGlassesMic: Boolean?,
423
- bypassVad: Boolean?,
424
434
  sendTranscript: Boolean?,
425
435
  sendLc3Data: Boolean? ->
426
436
  sdk?.setMicState(
427
437
  enabled = enabled,
428
438
  useGlassesMic = useGlassesMic ?: true,
429
- bypassVad = bypassVad ?: true,
430
439
  sendTranscript = sendTranscript ?: false,
431
440
  sendLc3Data = sendLc3Data ?: false,
432
441
  )
@@ -150,17 +150,23 @@ public class Bridge private constructor() {
150
150
  }
151
151
 
152
152
  private fun micPcmEventBody(data: ByteArray): HashMap<String, Any> {
153
+ val voiceActivityDetectionEnabled =
154
+ DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean
155
+ ?: true
153
156
  val body = HashMap<String, Any>()
154
157
  body["pcm"] = data
155
158
  body["sampleRate"] = MIC_SAMPLE_RATE
156
159
  body["bitsPerSample"] = PCM_BITS_PER_SAMPLE
157
160
  body["channels"] = MIC_CHANNELS
158
161
  body["encoding"] = "pcm_s16le"
159
- body["vadGated"] = isVadGated()
162
+ body["voiceActivityDetectionEnabled"] = voiceActivityDetectionEnabled
160
163
  return body
161
164
  }
162
165
 
163
166
  private fun micLc3EventBody(data: ByteArray): HashMap<String, Any> {
167
+ val voiceActivityDetectionEnabled =
168
+ DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean
169
+ ?: true
164
170
  val frameSizeBytes =
165
171
  (DeviceStore.store.get("bluetooth", "lc3_frame_size") as? Number)?.toInt()
166
172
  ?: DEFAULT_LC3_FRAME_SIZE_BYTES
@@ -173,13 +179,10 @@ public class Bridge private constructor() {
173
179
  body["frameSizeBytes"] = frameSizeBytes
174
180
  body["bitrate"] = frameSizeBytes * 8 * (1000 / LC3_FRAME_DURATION_MS)
175
181
  body["packetizedFromGlasses"] = false
176
- body["vadGated"] = isVadGated()
182
+ body["voiceActivityDetectionEnabled"] = voiceActivityDetectionEnabled
177
183
  return body
178
184
  }
179
185
 
180
- private fun isVadGated(): Boolean =
181
- !((DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean) ?: true)
182
-
183
186
  /** Save a setting */
184
187
  @JvmStatic
185
188
  fun saveSetting(key: String, value: Any) {
@@ -189,12 +192,22 @@ public class Bridge private constructor() {
189
192
  sendTypedMessage("save_setting", body as Map<String, Any>)
190
193
  }
191
194
 
192
- /** Send VAD (Voice Activity Detection) status */
195
+ /** Send Voice Activity Detection status */
196
+ @JvmStatic
197
+ fun sendVoiceActivityDetectionStatus(enabled: Boolean) {
198
+ DeviceStore.set("glasses", "voiceActivityDetectionEnabled", enabled)
199
+ val body = HashMap<String, Any>()
200
+ body["voiceActivityDetectionEnabled"] = enabled
201
+ sendTypedMessage("voice_activity_detection_status", body as Map<String, Any>)
202
+ }
203
+
204
+ /** Send live speaking status reported by glasses-side Voice Activity Detection. */
193
205
  @JvmStatic
194
- fun sendVadEvent(isSpeaking: Boolean) {
206
+ fun sendSpeakingStatus(speaking: Boolean) {
195
207
  val body = HashMap<String, Any>()
196
- body["status"] = isSpeaking
197
- sendTypedMessage("vad_status", body as Map<String, Any>)
208
+ body["speaking"] = speaking
209
+ body["timestamp"] = System.currentTimeMillis()
210
+ sendTypedMessage("speaking_status", body as Map<String, Any>)
198
211
  }
199
212
 
200
213
  /** Send battery status */
@@ -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,28 @@ 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,
1384
1356
  ) {
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)
1357
+ val exposureNs: Long? =
1358
+ exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
1359
+ when {
1360
+ v > Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
1361
+ else -> v.toLong()
1362
+ }
1363
+ }
1364
+ Bridge.log(
1365
+ "MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=$requestId appId=$appId size=$size compress=$compress flash=$flash sound=$sound exposureTimeNs=$exposureNs sgc=${sgc?.javaClass?.simpleName ?: "null"}"
1366
+ )
1367
+ val activeSgc = sgc
1368
+ if (activeSgc == null) {
1369
+ Bridge.log(
1370
+ "MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=$requestId"
1371
+ )
1372
+ return
1373
+ }
1374
+ activeSgc.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, sound, exposureNs)
1387
1375
  }
1388
1376
 
1389
1377
  fun rgbLedControl(
@@ -1513,6 +1501,7 @@ class DeviceManager {
1513
1501
  DeviceStore.apply("glasses", "deviceModel", "")
1514
1502
  DeviceStore.apply("glasses", "fullyBooted", false)
1515
1503
  DeviceStore.apply("glasses", "connected", false)
1504
+ DeviceStore.apply("glasses", "voiceActivityDetectionEnabled", true)
1516
1505
  // disconnect the controller as well:
1517
1506
  searchingController = false
1518
1507
  DeviceStore.apply("glasses", "controllerConnected", false)
@@ -66,6 +66,7 @@ object DeviceStore {
66
66
  store.set("glasses", "deviceModel", "")
67
67
  store.set("glasses", "firmwareVersion", "")
68
68
  store.set("glasses", "micEnabled", false)
69
+ store.set("glasses", "voiceActivityDetectionEnabled", true)
69
70
  store.set("glasses", "bluetoothClassicConnected", false)
70
71
  store.set("glasses", "caseRemoved", true)
71
72
  store.set("glasses", "caseOpen", true)
@@ -121,12 +122,13 @@ object DeviceStore {
121
122
  store.set("bluetooth", "dashboard_depth", 2)
122
123
  store.set("bluetooth", "head_up_angle", 30)
123
124
  store.set("bluetooth", "contextual_dashboard", true)
124
- store.set("bluetooth", "galleryModeAuto", true)
125
+ store.set("bluetooth", "gallery_mode", true)
126
+ store.set("bluetooth", "voice_activity_detection_enabled", true)
125
127
  store.set("bluetooth", "screen_disabled", false)
126
128
  store.set("bluetooth", "button_photo_size", "medium")
127
129
  store.set("bluetooth", "button_camera_led", true)
128
130
  store.set("bluetooth", "button_max_recording_time", 10)
129
- store.set("bluetooth", "camera_fov", mapOf("fov" to 118, "roiPosition" to 0))
131
+ store.set("bluetooth", "camera_fov", mapOf("fov" to 118, "roi_position" to 0))
130
132
  store.set("bluetooth", "button_video_width", 1280)
131
133
  store.set("bluetooth", "button_video_height", 720)
132
134
  store.set("bluetooth", "button_video_fps", 30)
@@ -137,7 +139,6 @@ object DeviceStore {
137
139
  store.set("bluetooth", "should_send_pcm", false)
138
140
  store.set("bluetooth", "should_send_lc3", false)
139
141
  store.set("bluetooth", "should_send_transcript", false)
140
- store.set("bluetooth", "bypass_vad", true)
141
142
  store.set("bluetooth", "use_native_dashboard", false)
142
143
  }
143
144
 
@@ -240,9 +241,12 @@ object DeviceStore {
240
241
  DeviceManager.getInstance().sgc?.setDashboardMenu(items)
241
242
  }
242
243
  }
243
- "bluetooth" to "galleryModeAuto" -> {
244
+ "bluetooth" to "gallery_mode" -> {
244
245
  DeviceManager.getInstance().sgc?.sendGalleryMode()
245
246
  }
247
+ "bluetooth" to "voice_activity_detection_enabled" -> {
248
+ DeviceManager.getInstance().sgc?.sendVoiceActivityDetectionSetting()
249
+ }
246
250
  "bluetooth" to "screen_disabled" -> {
247
251
  (value as? Boolean)?.let { disabled ->
248
252
  if (disabled) {
@@ -308,8 +308,12 @@ class MentraBluetoothSdk private constructor(
308
308
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "screen_disabled", disabled)
309
309
  }
310
310
 
311
- fun setGalleryMode(mode: GalleryMode) {
312
- DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "galleryModeAuto", mode == GalleryMode.AUTO)
311
+ fun setGalleryModeEnabled(enabled: Boolean) {
312
+ DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "gallery_mode", enabled)
313
+ }
314
+
315
+ fun setVoiceActivityDetectionEnabled(enabled: Boolean) {
316
+ DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "voice_activity_detection_enabled", enabled)
313
317
  }
314
318
 
315
319
  fun setButtonPhotoSettings(size: ButtonPhotoSize) {
@@ -320,14 +324,14 @@ class MentraBluetoothSdk private constructor(
320
324
  setButtonPhotoSettings(size = settings.size)
321
325
  }
322
326
 
323
- fun setButtonVideoRecordingSettings(width: Int, height: Int, frameRate: Int) {
327
+ fun setButtonVideoRecordingSettings(width: Int, height: Int, fps: Int) {
324
328
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "button_video_width", width)
325
329
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "button_video_height", height)
326
- DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", frameRate)
330
+ DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", fps)
327
331
  }
328
332
 
329
333
  fun setButtonVideoRecordingSettings(settings: ButtonVideoRecordingSettings) {
330
- setButtonVideoRecordingSettings(width = settings.width, height = settings.height, frameRate = settings.frameRate)
334
+ setButtonVideoRecordingSettings(width = settings.width, height = settings.height, fps = settings.fps)
331
335
  }
332
336
 
333
337
  fun setButtonCameraLed(enabled: Boolean) {
@@ -342,14 +346,13 @@ class MentraBluetoothSdk private constructor(
342
346
  DeviceStore.apply(
343
347
  ObservableStore.BLUETOOTH_CATEGORY,
344
348
  "camera_fov",
345
- mapOf("fov" to fov.fov, "roiPosition" to fov.roiPosition),
349
+ mapOf("fov" to fov.fov, "roi_position" to fov.roiPosition),
346
350
  )
347
351
  }
348
352
 
349
353
  fun setMicState(
350
354
  enabled: Boolean,
351
355
  useGlassesMic: Boolean = true,
352
- bypassVad: Boolean = true,
353
356
  sendTranscript: Boolean = false,
354
357
  sendLc3Data: Boolean = false,
355
358
  ) {
@@ -363,7 +366,6 @@ class MentraBluetoothSdk private constructor(
363
366
  applyMicState(
364
367
  sendPcmData = enabled,
365
368
  sendTranscript = enabled && sendTranscript,
366
- bypassVad = bypassVad,
367
369
  sendLc3Data = enabled && sendLc3Data,
368
370
  )
369
371
  }
@@ -371,13 +373,11 @@ class MentraBluetoothSdk private constructor(
371
373
  private fun applyMicState(
372
374
  sendPcmData: Boolean,
373
375
  sendTranscript: Boolean,
374
- bypassVad: Boolean,
375
376
  sendLc3Data: Boolean,
376
377
  ) {
377
378
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "should_send_pcm", sendPcmData)
378
379
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "should_send_lc3", sendLc3Data)
379
380
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "should_send_transcript", sendTranscript)
380
- DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "bypass_vad", bypassVad)
381
381
  deviceManager.setMicState()
382
382
  }
383
383
 
@@ -414,6 +414,9 @@ class MentraBluetoothSdk private constructor(
414
414
  }
415
415
 
416
416
  fun requestPhoto(request: PhotoRequest) {
417
+ Bridge.log(
418
+ "NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=${request.requestId} appId=${request.appId}"
419
+ )
417
420
  deviceManager.requestPhoto(
418
421
  request.requestId,
419
422
  request.appId,
@@ -421,7 +424,9 @@ class MentraBluetoothSdk private constructor(
421
424
  request.webhookUrl,
422
425
  request.authToken,
423
426
  request.compress.value,
427
+ request.flash,
424
428
  request.sound,
429
+ request.exposureTimeNs,
425
430
  )
426
431
  }
427
432
 
@@ -605,6 +610,25 @@ class MentraBluetoothSdk private constructor(
605
610
  }
606
611
  }
607
612
  "head_up" -> dispatchToListeners { it.onHeadUpChanged(data["up"] as? Boolean ?: false) }
613
+ "voice_activity_detection_status" ->
614
+ dispatchToListeners {
615
+ it.onVoiceActivityDetectionStatus(
616
+ VoiceActivityDetectionStatusEvent(
617
+ voiceActivityDetectionEnabled =
618
+ data["voiceActivityDetectionEnabled"] as? Boolean ?: true,
619
+ values = data,
620
+ )
621
+ )
622
+ }
623
+ "speaking_status" ->
624
+ dispatchToListeners {
625
+ it.onSpeakingStatus(
626
+ SpeakingStatusEvent(
627
+ speaking = data["speaking"] as? Boolean ?: false,
628
+ values = data,
629
+ )
630
+ )
631
+ }
608
632
  "battery_status" ->
609
633
  dispatchToListeners {
610
634
  it.onBatteryStatus(
@@ -13,7 +13,7 @@ data class MicPcmEvent(
13
13
  val bitsPerSample: Int,
14
14
  val channels: Int,
15
15
  val encoding: String,
16
- val vadGated: Boolean,
16
+ val voiceActivityDetectionEnabled: Boolean,
17
17
  ) {
18
18
  constructor(values: Map<String, Any>) : this(
19
19
  pcm = values["pcm"] as? ByteArray ?: ByteArray(0),
@@ -21,7 +21,7 @@ data class MicPcmEvent(
21
21
  bitsPerSample = numberValue(values, "bitsPerSample") ?: BITS_PER_SAMPLE,
22
22
  channels = numberValue(values, "channels") ?: CHANNELS,
23
23
  encoding = stringValue(values, "encoding") ?: ENCODING,
24
- vadGated = boolValue(values, "vadGated") ?: false,
24
+ voiceActivityDetectionEnabled = boolValue(values, "voiceActivityDetectionEnabled") ?: true,
25
25
  )
26
26
 
27
27
  fun toMap(): Map<String, Any> =
@@ -32,7 +32,7 @@ data class MicPcmEvent(
32
32
  "bitsPerSample" to bitsPerSample,
33
33
  "channels" to channels,
34
34
  "encoding" to encoding,
35
- "vadGated" to vadGated,
35
+ "voiceActivityDetectionEnabled" to voiceActivityDetectionEnabled,
36
36
  )
37
37
 
38
38
  companion object {
@@ -52,7 +52,7 @@ data class MicLc3Event(
52
52
  val frameSizeBytes: Int,
53
53
  val bitrate: Int,
54
54
  val packetizedFromGlasses: Boolean,
55
- val vadGated: Boolean,
55
+ val voiceActivityDetectionEnabled: Boolean,
56
56
  ) {
57
57
  constructor(values: Map<String, Any>) : this(
58
58
  lc3 = values["lc3"] as? ByteArray ?: ByteArray(0),
@@ -63,7 +63,7 @@ data class MicLc3Event(
63
63
  frameSizeBytes = numberValue(values, "frameSizeBytes") ?: DEFAULT_FRAME_SIZE_BYTES,
64
64
  bitrate = numberValue(values, "bitrate") ?: DEFAULT_BITRATE,
65
65
  packetizedFromGlasses = boolValue(values, "packetizedFromGlasses") ?: false,
66
- vadGated = boolValue(values, "vadGated") ?: false,
66
+ voiceActivityDetectionEnabled = boolValue(values, "voiceActivityDetectionEnabled") ?: true,
67
67
  )
68
68
 
69
69
  fun toMap(): Map<String, Any> =
@@ -77,7 +77,7 @@ data class MicLc3Event(
77
77
  "frameSizeBytes" to frameSizeBytes,
78
78
  "bitrate" to bitrate,
79
79
  "packetizedFromGlasses" to packetizedFromGlasses,
80
- "vadGated" to vadGated,
80
+ "voiceActivityDetectionEnabled" to voiceActivityDetectionEnabled,
81
81
  )
82
82
 
83
83
  companion object {