@mentra/bluetooth-sdk 0.1.5 → 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 +45 -27
- package/android/build.gradle +2 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +53 -36
- 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 +9 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +68 -56
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +125 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +209 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/connection/ConnectionModels.kt +78 -0
- 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 +72 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +132 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +39 -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 +607 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/RuntimeState.kt +201 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +173 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +348 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +75 -0
- package/build/BluetoothSdk.types.d.ts +46 -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 +40 -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 +45 -28
- package/ios/MentraBluetoothSDK.podspec +0 -3
- package/ios/Source/Audio/AudioModels.swift +159 -0
- package/ios/Source/Bridge.swift +22 -11
- package/ios/Source/Camera/CameraModels.swift +248 -0
- package/ios/Source/Connection/ScanSession.swift +27 -0
- package/ios/Source/DeviceManager.swift +20 -97
- package/ios/Source/DeviceStore.swift +13 -4
- package/ios/Source/Errors/BluetoothError.swift +19 -0
- package/ios/Source/Events/BluetoothEvents.swift +149 -0
- package/ios/Source/Internal/BluetoothAvailability.swift +58 -0
- package/ios/Source/Internal/ValueParsing.swift +90 -0
- package/ios/Source/MentraBluetoothSDK.swift +50 -2150
- package/ios/Source/Requests/DisplayRequests.swift +58 -0
- package/ios/Source/Status/DeviceStatus.swift +471 -0
- package/ios/Source/Status/RuntimeState.swift +359 -0
- package/ios/Source/Status/WifiHotspotStatus.swift +326 -0
- package/ios/Source/Streaming/StreamModels.swift +436 -0
- package/ios/Source/Types/DeviceModels.swift +134 -0
- 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 +99 -39
- 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 +48 -28
- package/src/_private/BluetoothSdkModule.ts +47 -21
- package/src/index.ts +6 -4
- package/src/react/useMentraBluetooth.ts +19 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +0 -1810
- 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
|
@@ -123,15 +123,21 @@ const devices = await BluetoothSdk.scan(DeviceModels.MentraLive, {
|
|
|
123
123
|
},
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
-
const device = devices
|
|
126
|
+
const device = await chooseDevice(devices)
|
|
127
127
|
if (!device) {
|
|
128
|
-
throw new Error('No Mentra Live glasses
|
|
128
|
+
throw new Error('No Mentra Live glasses selected')
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
await BluetoothSdk.connect(device)
|
|
132
|
-
await BluetoothSdk.
|
|
132
|
+
await BluetoothSdk.requestVersionInfo()
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
In multi-device environments, present an explicit picker instead of
|
|
136
|
+
auto-connecting to the first nearby device.
|
|
137
|
+
|
|
138
|
+
Mentra Live does not have a display. Use display commands only after gating by
|
|
139
|
+
model capability.
|
|
140
|
+
|
|
135
141
|
Use `Device.id` as the stable app-facing key for scan rows, selected devices,
|
|
136
142
|
and persisted default devices. Do not parse it for model, name, or address
|
|
137
143
|
information; use the typed `model`, `name`, `address`, and `rssi` fields
|
|
@@ -139,6 +145,10 @@ instead. Android commonly uses a Bluetooth address when available, iOS commonly
|
|
|
139
145
|
uses a CoreBluetooth identifier when available, and the SDK falls back to
|
|
140
146
|
`model:name` when no platform identifier is available.
|
|
141
147
|
|
|
148
|
+
`Device.rssi` is optional. A device can appear in scan results before the
|
|
149
|
+
platform reports RSSI, so picker UI should handle `undefined` and avoid
|
|
150
|
+
reordering rows just because RSSI metadata arrives later.
|
|
151
|
+
|
|
142
152
|
## React Hooks
|
|
143
153
|
|
|
144
154
|
React Native apps can import optional lifecycle helpers from the `react`
|
|
@@ -225,8 +235,8 @@ await BluetoothSdk.sendWifiCredentials('Office WiFi', 'secret')
|
|
|
225
235
|
await BluetoothSdk.forgetWifiNetwork('Office WiFi')
|
|
226
236
|
await BluetoothSdk.setHotspotState(true)
|
|
227
237
|
|
|
228
|
-
await BluetoothSdk.
|
|
229
|
-
await BluetoothSdk.
|
|
238
|
+
await BluetoothSdk.setGalleryModeEnabled(true)
|
|
239
|
+
await BluetoothSdk.setGalleryModeEnabled(false)
|
|
230
240
|
|
|
231
241
|
await BluetoothSdk.setPreferredMic('auto')
|
|
232
242
|
await BluetoothSdk.setMicState(true)
|
|
@@ -243,20 +253,20 @@ await BluetoothSdk.rgbLedControl(
|
|
|
243
253
|
)
|
|
244
254
|
```
|
|
245
255
|
|
|
246
|
-
`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.
|
|
247
257
|
|
|
248
258
|
## Photo Upload
|
|
249
259
|
|
|
250
260
|
```ts
|
|
251
|
-
await BluetoothSdk.requestPhoto(
|
|
252
|
-
`photo-${Date.now()}`,
|
|
253
|
-
'com.example.app',
|
|
254
|
-
'medium',
|
|
255
|
-
'https://api.example.com/mentra/photo',
|
|
256
|
-
'optional-token',
|
|
257
|
-
'medium',
|
|
258
|
-
true,
|
|
259
|
-
)
|
|
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
|
+
})
|
|
260
270
|
```
|
|
261
271
|
|
|
262
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.
|
|
@@ -270,6 +280,7 @@ await BluetoothSdk.startStream({
|
|
|
270
280
|
type: 'start_stream',
|
|
271
281
|
streamUrl: 'http://192.168.1.42:8889/mentra-live/whip',
|
|
272
282
|
streamId,
|
|
283
|
+
video: {fps: 15},
|
|
273
284
|
})
|
|
274
285
|
|
|
275
286
|
await BluetoothSdk.keepStreamAlive({
|
|
@@ -285,24 +296,31 @@ Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https:
|
|
|
285
296
|
|
|
286
297
|
## Events
|
|
287
298
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
299
|
+
React Native components should use `useBluetoothEvent()` for hardware events:
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
import {useBluetoothEvent} from '@mentra/bluetooth-sdk/react'
|
|
303
|
+
|
|
304
|
+
export function HardwareEventLogger() {
|
|
305
|
+
useBluetoothEvent('button_press', (event) => console.log(event))
|
|
306
|
+
useBluetoothEvent('touch_event', (event) => console.log(event))
|
|
307
|
+
useBluetoothEvent('photo_response', (event) => console.log(event))
|
|
308
|
+
useBluetoothEvent('stream_status', (event) => console.log(event))
|
|
309
|
+
useBluetoothEvent('speaking_status', (event) => console.log(event.speaking))
|
|
310
|
+
useBluetoothEvent('mic_pcm', (event) => {
|
|
295
311
|
console.log(event.sampleRate, event.bitsPerSample, event.channels, event.encoding)
|
|
296
312
|
console.log(event.pcm)
|
|
297
|
-
})
|
|
298
|
-
]
|
|
313
|
+
})
|
|
299
314
|
|
|
300
|
-
|
|
315
|
+
return null
|
|
316
|
+
}
|
|
301
317
|
```
|
|
302
318
|
|
|
319
|
+
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.
|
|
320
|
+
|
|
303
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`.
|
|
304
322
|
|
|
305
|
-
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`.
|
|
306
324
|
|
|
307
325
|
Only documented imports are supported for app developers. Undocumented package subpaths or symbols with a leading underscore can change without notice.
|
|
308
326
|
|
|
@@ -319,4 +337,4 @@ Use `bunx expo run:android` for Android. Keep local paths in your shell or CI en
|
|
|
319
337
|
|
|
320
338
|
## Starter Example App
|
|
321
339
|
|
|
322
|
-
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
|
}
|
|
@@ -9,12 +9,23 @@ class BluetoothSdkModule : Module() {
|
|
|
9
9
|
private var deviceManager: DeviceManager? = null
|
|
10
10
|
private val sdkListener =
|
|
11
11
|
object : MentraBluetoothSdkListener {
|
|
12
|
-
override fun
|
|
13
|
-
sendEvent(
|
|
12
|
+
override fun onGlassesChanged(glasses: GlassesRuntimeState) {
|
|
13
|
+
sendEvent(
|
|
14
|
+
"glasses_status",
|
|
15
|
+
sdk?.getRawGlassesStatus()?.toMap()
|
|
16
|
+
?: GlassesStatus.fromMap(DeviceStore.store.getCategory("glasses")).toMap()
|
|
17
|
+
)
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
override fun
|
|
17
|
-
sendEvent(
|
|
20
|
+
override fun onSdkStateChanged(sdkState: PhoneSdkRuntimeState) {
|
|
21
|
+
sendEvent(
|
|
22
|
+
"bluetooth_status",
|
|
23
|
+
sdk?.getRawBluetoothStatus()?.toMap()
|
|
24
|
+
?: BluetoothStatus.fromMap(
|
|
25
|
+
DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
|
|
26
|
+
)
|
|
27
|
+
.toMap()
|
|
28
|
+
)
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
override fun onDeviceDiscovered(device: Device) {
|
|
@@ -31,7 +42,7 @@ class BluetoothSdkModule : Module() {
|
|
|
31
42
|
|
|
32
43
|
override fun onScanStopped(reason: ScanStopReason) {
|
|
33
44
|
if (reason == ScanStopReason.COMPLETED) {
|
|
34
|
-
val status = sdk?.
|
|
45
|
+
val status = sdk?.getRawBluetoothStatus()
|
|
35
46
|
val deviceModel =
|
|
36
47
|
status?.pendingWearable?.takeIf { it.isNotBlank() }
|
|
37
48
|
?: status?.defaultWearable
|
|
@@ -65,6 +76,14 @@ class BluetoothSdkModule : Module() {
|
|
|
65
76
|
sendEvent("head_up", mapOf("up" to headUp))
|
|
66
77
|
}
|
|
67
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
|
+
|
|
68
87
|
override fun onBatteryStatus(event: BatteryStatusEvent) {
|
|
69
88
|
sendEvent("battery_status", event.values)
|
|
70
89
|
}
|
|
@@ -137,7 +156,8 @@ class BluetoothSdkModule : Module() {
|
|
|
137
156
|
"button_press",
|
|
138
157
|
"touch_event",
|
|
139
158
|
"head_up",
|
|
140
|
-
"
|
|
159
|
+
"voice_activity_detection_status",
|
|
160
|
+
"speaking_status",
|
|
141
161
|
"battery_status",
|
|
142
162
|
"local_transcription",
|
|
143
163
|
"wifi_status_change",
|
|
@@ -196,12 +216,14 @@ class BluetoothSdkModule : Module() {
|
|
|
196
216
|
// MARK: - Observable Store Functions
|
|
197
217
|
|
|
198
218
|
Function("getGlassesStatus") {
|
|
199
|
-
sdk?.
|
|
219
|
+
sdk?.getRawGlassesStatus()?.toMap()
|
|
200
220
|
?: GlassesStatus.fromMap(DeviceStore.store.getCategory("glasses")).toMap()
|
|
201
221
|
}
|
|
202
222
|
|
|
203
223
|
Function("getBluetoothStatus") {
|
|
204
|
-
sdk?.
|
|
224
|
+
sdk?.getRawBluetoothStatus()?.toMap()
|
|
225
|
+
?: BluetoothStatus.fromMap(DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY))
|
|
226
|
+
.toMap()
|
|
205
227
|
}
|
|
206
228
|
|
|
207
229
|
Function("getDefaultDevice") { sdk?.getDefaultDevice()?.toMap() }
|
|
@@ -336,37 +358,34 @@ class BluetoothSdkModule : Module() {
|
|
|
336
358
|
|
|
337
359
|
// MARK: - Gallery Commands
|
|
338
360
|
|
|
339
|
-
AsyncFunction("
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
sdk?.setGalleryMode(galleryMode)
|
|
361
|
+
AsyncFunction("setGalleryModeEnabled") { enabled: Boolean ->
|
|
362
|
+
sdk?.setGalleryModeEnabled(enabled)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
AsyncFunction("setVoiceActivityDetectionEnabled") { enabled: Boolean ->
|
|
366
|
+
sdk?.setVoiceActivityDetectionEnabled(enabled)
|
|
347
367
|
}
|
|
348
368
|
|
|
349
369
|
AsyncFunction("queryGalleryStatus") { sdk?.queryGalleryStatus() }
|
|
350
370
|
|
|
351
|
-
AsyncFunction("requestPhoto") {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
PhotoRequest(
|
|
361
|
-
requestId = requestId,
|
|
362
|
-
appId = appId,
|
|
363
|
-
size = PhotoSize.fromValue(size),
|
|
364
|
-
webhookUrl = webhookUrl,
|
|
365
|
-
authToken = authToken,
|
|
366
|
-
compress = PhotoCompression.fromValue(compress),
|
|
367
|
-
sound = sound,
|
|
368
|
-
)
|
|
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}"
|
|
369
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
|
+
}
|
|
370
389
|
}
|
|
371
390
|
|
|
372
391
|
// MARK: - OTA Commands
|
|
@@ -412,13 +431,11 @@ class BluetoothSdkModule : Module() {
|
|
|
412
431
|
AsyncFunction("setMicState") {
|
|
413
432
|
enabled: Boolean,
|
|
414
433
|
useGlassesMic: Boolean?,
|
|
415
|
-
bypassVad: Boolean?,
|
|
416
434
|
sendTranscript: Boolean?,
|
|
417
435
|
sendLc3Data: Boolean? ->
|
|
418
436
|
sdk?.setMicState(
|
|
419
437
|
enabled = enabled,
|
|
420
438
|
useGlassesMic = useGlassesMic ?: true,
|
|
421
|
-
bypassVad = bypassVad ?: true,
|
|
422
439
|
sendTranscript = sendTranscript ?: false,
|
|
423
440
|
sendLc3Data = sendLc3Data ?: false,
|
|
424
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,7 @@ 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", "
|
|
142
|
+
store.set("bluetooth", "use_native_dashboard", false)
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
fun get(category: String, key: String): Any? {
|
|
@@ -239,9 +241,12 @@ object DeviceStore {
|
|
|
239
241
|
DeviceManager.getInstance().sgc?.setDashboardMenu(items)
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
|
-
"bluetooth" to "
|
|
244
|
+
"bluetooth" to "gallery_mode" -> {
|
|
243
245
|
DeviceManager.getInstance().sgc?.sendGalleryMode()
|
|
244
246
|
}
|
|
247
|
+
"bluetooth" to "voice_activity_detection_enabled" -> {
|
|
248
|
+
DeviceManager.getInstance().sgc?.sendVoiceActivityDetectionSetting()
|
|
249
|
+
}
|
|
245
250
|
"bluetooth" to "screen_disabled" -> {
|
|
246
251
|
(value as? Boolean)?.let { disabled ->
|
|
247
252
|
if (disabled) {
|