@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.
- package/README.md +16 -14
- package/android/build.gradle +2 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +40 -31
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +22 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +25 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +29 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +4 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +168 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +7 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +3 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +4 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +3 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -3
- package/build/BluetoothSdk.types.d.ts +42 -20
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +6 -5
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +37 -5
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -2
- package/build/index.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +5 -3
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +15 -4
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +41 -24
- package/ios/MentraBluetoothSDK.podspec +0 -3
- package/ios/Source/Audio/AudioModels.swift +10 -10
- package/ios/Source/Bridge.swift +22 -11
- package/ios/Source/Camera/CameraModels.swift +13 -11
- package/ios/Source/DeviceManager.swift +20 -97
- package/ios/Source/DeviceStore.swift +12 -4
- package/ios/Source/Events/BluetoothEvents.swift +34 -0
- package/ios/Source/MentraBluetoothSDK.swift +25 -10
- package/ios/Source/Status/DeviceStatus.swift +12 -4
- package/ios/Source/Status/RuntimeState.swift +22 -13
- package/ios/Source/Streaming/StreamModels.swift +5 -5
- package/ios/Source/controllers/ControllerManager.swift +3 -3
- package/ios/Source/controllers/R1.swift +1 -1
- package/ios/Source/sgcs/G1.swift +1 -1
- package/ios/Source/sgcs/G2.swift +2 -3
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +109 -12
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +11 -3
- package/ios/Source/sgcs/Simulated.swift +2 -2
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +1 -4
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +44 -28
- package/src/_private/BluetoothSdkModule.ts +44 -21
- package/src/index.ts +6 -4
- package/src/react/useMentraBluetooth.ts +19 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +0 -221
- package/ios/Packages/VAD/Common/VAD/VADQuality.swift +0 -41
- package/ios/Packages/VAD/Common/VAD/VADState.swift +0 -26
- package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +0 -29
- package/ios/Packages/VAD/Common/VAD/VADType.swift +0 -14
- package/ios/Packages/VAD/Data/Configuration.swift +0 -68
- package/ios/Packages/VAD/Data/FrameSize.swift +0 -39
- package/ios/Packages/VAD/Data/Record.swift +0 -13
- package/ios/Packages/VAD/Data/Result.swift +0 -22
- package/ios/Packages/VAD/Data/SampleRate.swift +0 -48
- package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
- package/ios/Packages/VAD/Silero/SileroVAD.swift +0 -284
- 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.
|
|
239
|
-
await BluetoothSdk.
|
|
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.
|
|
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`,
|
|
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
|
|
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.
|
package/android/build.gradle
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
?:
|
|
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
|
-
"
|
|
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()
|
|
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("
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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["
|
|
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["
|
|
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
|
|
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
|
|
206
|
+
fun sendSpeakingStatus(speaking: Boolean) {
|
|
195
207
|
val body = HashMap<String, Any>()
|
|
196
|
-
body["
|
|
197
|
-
|
|
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", "
|
|
173
|
-
set(value) = DeviceStore.apply("bluetooth", "
|
|
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
|
-
|
|
1353
|
+
flash: Boolean,
|
|
1354
|
+
sound: Boolean,
|
|
1355
|
+
exposureTimeNs: Double? = null,
|
|
1384
1356
|
) {
|
|
1385
|
-
|
|
1386
|
-
|
|
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", "
|
|
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, "
|
|
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 "
|
|
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
|
|
312
|
-
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "
|
|
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,
|
|
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",
|
|
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,
|
|
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, "
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
80
|
+
"voiceActivityDetectionEnabled" to voiceActivityDetectionEnabled,
|
|
81
81
|
)
|
|
82
82
|
|
|
83
83
|
companion object {
|