@mentra/bluetooth-sdk 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -22
- package/android/.project +28 -0
- package/android/build.gradle +5 -9
- package/android/lc3Lib/.project +28 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +54 -33
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +48 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +27 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +170 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +69 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +5 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +149 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +297 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +120 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +13 -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 +3 -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 +269 -28
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +4 -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 +131 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +105 -0
- package/build/BluetoothSdk.types.d.ts +107 -48
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +4 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +8 -6
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +58 -9
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6 -5
- package/build/index.js.map +1 -1
- package/build/photo-receiver/MentraPhotoReceiver.types.d.ts +18 -0
- package/build/photo-receiver/MentraPhotoReceiver.types.d.ts.map +1 -0
- package/build/photo-receiver/MentraPhotoReceiver.types.js +2 -0
- package/build/photo-receiver/MentraPhotoReceiver.types.js.map +1 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.d.ts +10 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.d.ts.map +1 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.js +3 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.js.map +1 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.web.d.ts +10 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.web.d.ts.map +1 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.web.js +12 -0
- package/build/photo-receiver/MentraPhotoReceiverModule.web.js.map +1 -0
- package/build/photo-receiver/index.d.ts +3 -0
- package/build/photo-receiver/index.d.ts.map +1 -0
- package/build/photo-receiver/index.js +3 -0
- package/build/photo-receiver/index.js.map +1 -0
- 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/expo-module.config.json +5 -2
- package/ios/BluetoothSdkModule.swift +59 -26
- package/ios/LocalPhotoUploadServer.swift +411 -0
- package/ios/MentraBluetoothSDK.podspec +8 -5
- package/ios/MentraPhotoReceiverModule.swift +147 -0
- package/ios/Source/Audio/AudioModels.swift +10 -10
- package/ios/Source/Bridge.swift +42 -12
- package/ios/Source/Camera/CameraModels.swift +74 -19
- package/ios/Source/DeviceManager.swift +23 -98
- package/ios/Source/DeviceStore.swift +12 -4
- package/ios/Source/Events/BluetoothEvents.swift +37 -0
- package/ios/Source/MentraBluetoothSDK.swift +135 -14
- package/ios/Source/Status/DeviceStatus.swift +12 -4
- package/ios/Source/Status/RuntimeState.swift +22 -13
- package/ios/Source/Streaming/StreamModels.swift +239 -45
- package/ios/Source/controllers/ControllerManager.swift +3 -3
- package/ios/Source/controllers/R1.swift +9 -3
- package/ios/Source/sgcs/G1.swift +5 -4
- package/ios/Source/sgcs/G2.swift +5 -6
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +183 -22
- 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/ios/Source/utils/MessageChunkReassembler.swift +89 -0
- package/package.json +6 -1
- package/src/BluetoothSdk.types.ts +133 -62
- package/src/_private/BluetoothSdkModule.ts +79 -26
- package/src/index.ts +15 -8
- package/src/photo-receiver/MentraPhotoReceiver.types.ts +20 -0
- package/src/photo-receiver/MentraPhotoReceiverModule.ts +14 -0
- package/src/photo-receiver/MentraPhotoReceiverModule.web.ts +20 -0
- package/src/photo-receiver/index.ts +2 -0
- 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,24 +253,30 @@ 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
|
+
exposureTimeNs: null, // auto exposure; pass a positive nanosecond value for manual exposure
|
|
270
|
+
iso: null, // auto ISO; pass a positive ISO only with manual exposureTimeNs
|
|
271
|
+
})
|
|
270
272
|
```
|
|
271
273
|
|
|
272
274
|
The webhook should accept multipart form data with a `photo` file and `requestId`. If `authToken` is provided, the uploader adds `Authorization: Bearer <token>`. The camera light is always enabled for photo capture.
|
|
273
275
|
|
|
276
|
+
For one-shot manual capture tuning, pass `exposureTimeNs` and `iso` together. `exposureTimeNs` is sensor exposure time in nanoseconds; `iso` is sensor ISO. If `exposureTimeNs` is omitted, `null`, invalid, or unsupported by the connected glasses, the camera uses auto exposure and ignores `iso`.
|
|
277
|
+
|
|
278
|
+
Use `setCameraFov({fov, roiPosition})` to configure Mentra Live camera field of view and crop position. FOV is clamped to 82-118 degrees; ROI position is `0` center, `1` bottom, or `2` top. Applying FOV/ROI restarts the camera for about 5 seconds, so wait before requesting the next photo.
|
|
279
|
+
|
|
274
280
|
## Streaming
|
|
275
281
|
|
|
276
282
|
```ts
|
|
@@ -280,18 +286,14 @@ await BluetoothSdk.startStream({
|
|
|
280
286
|
type: 'start_stream',
|
|
281
287
|
streamUrl: 'http://192.168.1.42:8889/mentra-live/whip',
|
|
282
288
|
streamId,
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
await BluetoothSdk.keepStreamAlive({
|
|
286
|
-
type: 'keep_stream_alive',
|
|
287
|
-
streamId,
|
|
288
|
-
ackId: `ack-${Date.now()}`,
|
|
289
|
+
video: {fps: 15},
|
|
289
290
|
})
|
|
290
291
|
|
|
291
292
|
await BluetoothSdk.stopStream()
|
|
292
293
|
```
|
|
293
294
|
|
|
294
|
-
Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https://` for WHIP/WebRTC ingest.
|
|
295
|
+
Use `rtmp://` or `rtmps://` for RTMP, `srt://` for SRT, and `http://` or `https://` for WHIP/WebRTC ingest. The SDK sends stream keep-alives automatically while streaming and reports keep-alive failures through `stream_status`. The camera light is always enabled while streaming.
|
|
296
|
+
`stream_status` events may include `resolvedConfig`, which reports the effective transport, video, and audio settings after glasses defaults, clamps, and camera preflight.
|
|
295
297
|
|
|
296
298
|
## Events
|
|
297
299
|
|
|
@@ -303,8 +305,10 @@ import {useBluetoothEvent} from '@mentra/bluetooth-sdk/react'
|
|
|
303
305
|
export function HardwareEventLogger() {
|
|
304
306
|
useBluetoothEvent('button_press', (event) => console.log(event))
|
|
305
307
|
useBluetoothEvent('touch_event', (event) => console.log(event))
|
|
308
|
+
useBluetoothEvent('photo_status', (event) => console.log(event.status, event.resolvedConfig))
|
|
306
309
|
useBluetoothEvent('photo_response', (event) => console.log(event))
|
|
307
310
|
useBluetoothEvent('stream_status', (event) => console.log(event))
|
|
311
|
+
useBluetoothEvent('speaking_status', (event) => console.log(event.speaking))
|
|
308
312
|
useBluetoothEvent('mic_pcm', (event) => {
|
|
309
313
|
console.log(event.sampleRate, event.bitsPerSample, event.channels, event.encoding)
|
|
310
314
|
console.log(event.pcm)
|
|
@@ -316,9 +320,9 @@ export function HardwareEventLogger() {
|
|
|
316
320
|
|
|
317
321
|
For non-React modules, `BluetoothSdk.addListener(...)` is the low-level subscription API. Keep the returned subscription and call `remove()` when the listener is no longer needed.
|
|
318
322
|
|
|
319
|
-
Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `hotspot_status_change`, `photo_response`, `gallery_status`, `stream_status`, `
|
|
323
|
+
Common event names include `button_press`, `touch_event`, `head_up`, `battery_status`, `wifi_status_change`, `hotspot_status_change`, `photo_status`, `photo_response`, `gallery_status`, `stream_status`, `mic_pcm`, `mic_lc3`, `local_transcription`, `rgb_led_control_response`, `audio_connected`, `audio_disconnected`, and `log`.
|
|
320
324
|
|
|
321
|
-
React Native event payload fields use camelCase. For example, `touch_event` includes `gestureName`, `photo_response` success includes `uploadUrl`, and `gallery_status` includes `hasContent` and `cameraBusy`. `mic_pcm` includes `sampleRate`, `bitsPerSample`, `channels`,
|
|
325
|
+
React Native event payload fields use camelCase. For example, `touch_event` includes `gestureName`, `photo_response` success includes `uploadUrl`, and `gallery_status` includes `hasContent` and `cameraBusy`. `photo_status` reports intermediate photo states such as `accepted`, `queued`, `configuring`, `capturing`, `captured`, `compressing`, `uploading`, `ready_for_transfer`, `transferring`, and `failed`; the `configuring` event includes `resolvedConfig` with the effective JPEG dimensions, quality, requested size, transfer method, compression, and manual exposure fields when present. `mic_pcm` includes `sampleRate`, `bitsPerSample`, `channels`, and `encoding`; `mic_lc3` includes `sampleRate`, `channels`, `encoding`, `frameDurationMs`, `frameSizeBytes`, `bitrate`, and `packetizedFromGlasses`.
|
|
322
326
|
|
|
323
327
|
Only documented imports are supported for app developers. Undocumented package subpaths or symbols with a leading underscore can change without notice.
|
|
324
328
|
|
|
@@ -335,4 +339,4 @@ Use `bunx expo run:android` for Android. Keep local paths in your shell or CI en
|
|
|
335
339
|
|
|
336
340
|
## Starter Example App
|
|
337
341
|
|
|
338
|
-
The [Mentra Bluetooth SDK Starter Kit](https://github.com/Mentra-Community/Mentra-Bluetooth-SDK-Starter-Kit) includes starter example apps for Android, iOS, and React Native / Expo. The React Native starter demonstrates scan/connect, display, camera photo upload, RTMP/SRT/WebRTC streaming, Wi-Fi/hotspot, microphone PCM, RGB LED, gallery
|
|
342
|
+
The [Mentra Bluetooth SDK Starter Kit](https://github.com/Mentra-Community/Mentra-Bluetooth-SDK-Starter-Kit) includes starter example apps for Android, iOS, and React Native / Expo. The React Native starter demonstrates scan/connect, display, camera photo upload, RTMP/SRT/WebRTC streaming, Wi-Fi/hotspot, microphone PCM, RGB LED, gallery mode, and console event inspection.
|
package/android/.project
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<projectDescription>
|
|
3
|
+
<name>mentra-bluetooth-sdk</name>
|
|
4
|
+
<comment>Project mentra-bluetooth-sdk created by Buildship.</comment>
|
|
5
|
+
<projects>
|
|
6
|
+
</projects>
|
|
7
|
+
<buildSpec>
|
|
8
|
+
<buildCommand>
|
|
9
|
+
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
|
10
|
+
<arguments>
|
|
11
|
+
</arguments>
|
|
12
|
+
</buildCommand>
|
|
13
|
+
</buildSpec>
|
|
14
|
+
<natures>
|
|
15
|
+
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
|
16
|
+
</natures>
|
|
17
|
+
<filteredResources>
|
|
18
|
+
<filter>
|
|
19
|
+
<id>1779400133450</id>
|
|
20
|
+
<name></name>
|
|
21
|
+
<type>30</type>
|
|
22
|
+
<matcher>
|
|
23
|
+
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
|
24
|
+
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
|
25
|
+
</matcher>
|
|
26
|
+
</filter>
|
|
27
|
+
</filteredResources>
|
|
28
|
+
</projectDescription>
|
package/android/build.gradle
CHANGED
|
@@ -83,19 +83,15 @@ dependencies {
|
|
|
83
83
|
// OkHttp for HTTP requests
|
|
84
84
|
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
|
85
85
|
|
|
86
|
+
// Local photo upload receiver
|
|
87
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
|
|
88
|
+
|
|
86
89
|
// LC3 audio codec library
|
|
87
90
|
implementation project(':lc3Lib')
|
|
88
91
|
|
|
89
92
|
// Sherpa-ONNX for local transcription
|
|
90
93
|
implementation 'com.github.k2-fsa:sherpa-onnx:v1.12.8'
|
|
91
94
|
|
|
92
|
-
// Silero VAD (with onnxruntime exclusions to avoid conflicts with sherpa-onnx)
|
|
93
|
-
implementation ('com.github.gkonovalov:android-vad:2.0.9') {
|
|
94
|
-
exclude group: 'com.microsoft.onnxruntime', module: 'onnxruntime'
|
|
95
|
-
exclude group: 'com.microsoft.onnxruntime', module: 'onnxruntime4j'
|
|
96
|
-
exclude group: 'com.microsoft.onnxruntime', module: 'onnxruntime4j-jni'
|
|
97
|
-
}
|
|
98
|
-
|
|
99
95
|
// Vuzix Z100
|
|
100
96
|
implementation 'com.vuzix:ultralite-sdk-android:1.8'
|
|
101
97
|
implementation 'com.squareup.picasso:picasso:2.8'
|
|
@@ -110,11 +106,11 @@ configurations.all {
|
|
|
110
106
|
eachDependency { DependencyResolveDetails details ->
|
|
111
107
|
if (details.requested.group == 'com.microsoft.onnxruntime') {
|
|
112
108
|
details.useVersion '1.17.1'
|
|
113
|
-
details.because('Force override
|
|
109
|
+
details.because('Force override transitive onnxruntime version')
|
|
114
110
|
}
|
|
115
111
|
if (details.requested.name == 'onnxruntime-android') {
|
|
116
112
|
details.useVersion '1.17.1'
|
|
117
|
-
details.because('Force override
|
|
113
|
+
details.because('Force override transitive onnxruntime version')
|
|
118
114
|
}
|
|
119
115
|
}
|
|
120
116
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<projectDescription>
|
|
3
|
+
<name>lc3Lib</name>
|
|
4
|
+
<comment>Project lc3Lib created by Buildship.</comment>
|
|
5
|
+
<projects>
|
|
6
|
+
</projects>
|
|
7
|
+
<buildSpec>
|
|
8
|
+
<buildCommand>
|
|
9
|
+
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
|
10
|
+
<arguments>
|
|
11
|
+
</arguments>
|
|
12
|
+
</buildCommand>
|
|
13
|
+
</buildSpec>
|
|
14
|
+
<natures>
|
|
15
|
+
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
|
16
|
+
</natures>
|
|
17
|
+
<filteredResources>
|
|
18
|
+
<filter>
|
|
19
|
+
<id>1779400133449</id>
|
|
20
|
+
<name></name>
|
|
21
|
+
<type>30</type>
|
|
22
|
+
<matcher>
|
|
23
|
+
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
|
24
|
+
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
|
25
|
+
</matcher>
|
|
26
|
+
</filter>
|
|
27
|
+
</filteredResources>
|
|
28
|
+
</projectDescription>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk
|
|
2
2
|
|
|
3
|
+
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
3
4
|
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
4
5
|
import expo.modules.kotlin.modules.Module
|
|
5
6
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
@@ -21,7 +22,10 @@ class BluetoothSdkModule : Module() {
|
|
|
21
22
|
sendEvent(
|
|
22
23
|
"bluetooth_status",
|
|
23
24
|
sdk?.getRawBluetoothStatus()?.toMap()
|
|
24
|
-
?:
|
|
25
|
+
?: BluetoothStatus.fromMap(
|
|
26
|
+
DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY)
|
|
27
|
+
)
|
|
28
|
+
.toMap()
|
|
25
29
|
)
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -73,6 +77,14 @@ class BluetoothSdkModule : Module() {
|
|
|
73
77
|
sendEvent("head_up", mapOf("up" to headUp))
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
override fun onVoiceActivityDetectionStatus(event: VoiceActivityDetectionStatusEvent) {
|
|
81
|
+
sendEvent("voice_activity_detection_status", event.values)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override fun onSpeakingStatus(event: SpeakingStatusEvent) {
|
|
85
|
+
sendEvent("speaking_status", event.values)
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
override fun onBatteryStatus(event: BatteryStatusEvent) {
|
|
77
89
|
sendEvent("battery_status", event.values)
|
|
78
90
|
}
|
|
@@ -97,6 +109,10 @@ class BluetoothSdkModule : Module() {
|
|
|
97
109
|
sendEvent("photo_response", event.values)
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
override fun onPhotoStatus(event: PhotoStatusEvent) {
|
|
113
|
+
sendEvent("photo_status", event.values)
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
override fun onStreamStatus(event: StreamStatusEvent) {
|
|
101
117
|
sendEvent("stream_status", event.values)
|
|
102
118
|
}
|
|
@@ -145,13 +161,15 @@ class BluetoothSdkModule : Module() {
|
|
|
145
161
|
"button_press",
|
|
146
162
|
"touch_event",
|
|
147
163
|
"head_up",
|
|
148
|
-
"
|
|
164
|
+
"voice_activity_detection_status",
|
|
165
|
+
"speaking_status",
|
|
149
166
|
"battery_status",
|
|
150
167
|
"local_transcription",
|
|
151
168
|
"wifi_status_change",
|
|
152
169
|
"hotspot_status_change",
|
|
153
170
|
"hotspot_error",
|
|
154
171
|
"photo_response",
|
|
172
|
+
"photo_status",
|
|
155
173
|
"gallery_status",
|
|
156
174
|
"compatible_glasses_search_stop",
|
|
157
175
|
"heartbeat_sent",
|
|
@@ -191,11 +209,17 @@ class BluetoothSdkModule : Module() {
|
|
|
191
209
|
appContext.reactContext
|
|
192
210
|
?: appContext.currentActivity
|
|
193
211
|
?: throw IllegalStateException("No context available")
|
|
212
|
+
BleTraceLogger.logLifecycle(context, "BluetoothSdkModule", "module_create")
|
|
194
213
|
sdk = MentraBluetoothSdk.create(context, sdkListener)
|
|
195
214
|
deviceManager = DeviceManager.getInstance()
|
|
196
215
|
}
|
|
197
216
|
|
|
198
217
|
OnDestroy {
|
|
218
|
+
BleTraceLogger.logLifecycle(
|
|
219
|
+
appContext.reactContext ?: appContext.currentActivity,
|
|
220
|
+
"BluetoothSdkModule",
|
|
221
|
+
"module_destroy"
|
|
222
|
+
)
|
|
199
223
|
sdk?.close()
|
|
200
224
|
sdk = null
|
|
201
225
|
deviceManager = null
|
|
@@ -209,7 +233,9 @@ class BluetoothSdkModule : Module() {
|
|
|
209
233
|
}
|
|
210
234
|
|
|
211
235
|
Function("getBluetoothStatus") {
|
|
212
|
-
sdk?.getRawBluetoothStatus()?.toMap()
|
|
236
|
+
sdk?.getRawBluetoothStatus()?.toMap()
|
|
237
|
+
?: BluetoothStatus.fromMap(DeviceStore.store.getCategory(ObservableStore.BLUETOOTH_CATEGORY))
|
|
238
|
+
.toMap()
|
|
213
239
|
}
|
|
214
240
|
|
|
215
241
|
Function("getDefaultDevice") { sdk?.getDefaultDevice()?.toMap() }
|
|
@@ -344,37 +370,34 @@ class BluetoothSdkModule : Module() {
|
|
|
344
370
|
|
|
345
371
|
// MARK: - Gallery Commands
|
|
346
372
|
|
|
347
|
-
AsyncFunction("
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
sdk?.setGalleryMode(galleryMode)
|
|
373
|
+
AsyncFunction("setGalleryModeEnabled") { enabled: Boolean ->
|
|
374
|
+
sdk?.setGalleryModeEnabled(enabled)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
AsyncFunction("setVoiceActivityDetectionEnabled") { enabled: Boolean ->
|
|
378
|
+
sdk?.setVoiceActivityDetectionEnabled(enabled)
|
|
355
379
|
}
|
|
356
380
|
|
|
357
381
|
AsyncFunction("queryGalleryStatus") { sdk?.queryGalleryStatus() }
|
|
358
382
|
|
|
359
|
-
AsyncFunction("requestPhoto") {
|
|
360
|
-
|
|
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
|
-
)
|
|
383
|
+
AsyncFunction("requestPhoto") { params: Map<String, Any?> ->
|
|
384
|
+
// JS may pass null for optional fields; Map<String, Any> rejects null values at the bridge.
|
|
385
|
+
val sanitized =
|
|
386
|
+
params.mapNotNull { (key, value) ->
|
|
387
|
+
if (value == null) null else key to value
|
|
388
|
+
}.toMap()
|
|
389
|
+
val req = PhotoRequest.fromMap(sanitized)
|
|
390
|
+
Bridge.log(
|
|
391
|
+
"NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} appId=${req.appId} size=${req.size} compress=${req.compress} flash=${req.flash} sound=${req.sound} exposureTimeNs=${req.exposureTimeNs} iso=${req.iso}"
|
|
377
392
|
)
|
|
393
|
+
val activeSdk = sdk
|
|
394
|
+
if (activeSdk == null) {
|
|
395
|
+
Bridge.log(
|
|
396
|
+
"NATIVE: PHOTO PIPELINE — sdk is null; requestPhoto dropped requestId=${req.requestId}"
|
|
397
|
+
)
|
|
398
|
+
} else {
|
|
399
|
+
activeSdk.requestPhoto(req)
|
|
400
|
+
}
|
|
378
401
|
}
|
|
379
402
|
|
|
380
403
|
// MARK: - OTA Commands
|
|
@@ -411,8 +434,8 @@ class BluetoothSdkModule : Module() {
|
|
|
411
434
|
|
|
412
435
|
AsyncFunction("stopStream") { sdk?.stopStream() }
|
|
413
436
|
|
|
414
|
-
AsyncFunction("
|
|
415
|
-
sdk?.
|
|
437
|
+
AsyncFunction("sendCloudStreamKeepAlive") { params: Map<String, Any> ->
|
|
438
|
+
sdk?.sendCloudStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
|
|
416
439
|
}
|
|
417
440
|
|
|
418
441
|
// MARK: - Microphone Commands
|
|
@@ -420,13 +443,11 @@ class BluetoothSdkModule : Module() {
|
|
|
420
443
|
AsyncFunction("setMicState") {
|
|
421
444
|
enabled: Boolean,
|
|
422
445
|
useGlassesMic: Boolean?,
|
|
423
|
-
bypassVad: Boolean?,
|
|
424
446
|
sendTranscript: Boolean?,
|
|
425
447
|
sendLc3Data: Boolean? ->
|
|
426
448
|
sdk?.setMicState(
|
|
427
449
|
enabled = enabled,
|
|
428
450
|
useGlassesMic = useGlassesMic ?: true,
|
|
429
|
-
bypassVad = bypassVad ?: true,
|
|
430
451
|
sendTranscript = sendTranscript ?: false,
|
|
431
452
|
sendLc3Data = sendLc3Data ?: false,
|
|
432
453
|
)
|
|
@@ -9,6 +9,7 @@ package com.mentra.bluetoothsdk
|
|
|
9
9
|
|
|
10
10
|
import android.util.Base64
|
|
11
11
|
import android.util.Log
|
|
12
|
+
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
12
13
|
import java.util.HashMap
|
|
13
14
|
import java.util.UUID
|
|
14
15
|
import kotlin.jvm.JvmStatic
|
|
@@ -150,17 +151,23 @@ public class Bridge private constructor() {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
private fun micPcmEventBody(data: ByteArray): HashMap<String, Any> {
|
|
154
|
+
val voiceActivityDetectionEnabled =
|
|
155
|
+
DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean
|
|
156
|
+
?: true
|
|
153
157
|
val body = HashMap<String, Any>()
|
|
154
158
|
body["pcm"] = data
|
|
155
159
|
body["sampleRate"] = MIC_SAMPLE_RATE
|
|
156
160
|
body["bitsPerSample"] = PCM_BITS_PER_SAMPLE
|
|
157
161
|
body["channels"] = MIC_CHANNELS
|
|
158
162
|
body["encoding"] = "pcm_s16le"
|
|
159
|
-
body["
|
|
163
|
+
body["voiceActivityDetectionEnabled"] = voiceActivityDetectionEnabled
|
|
160
164
|
return body
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
private fun micLc3EventBody(data: ByteArray): HashMap<String, Any> {
|
|
168
|
+
val voiceActivityDetectionEnabled =
|
|
169
|
+
DeviceStore.get("glasses", "voiceActivityDetectionEnabled") as? Boolean
|
|
170
|
+
?: true
|
|
164
171
|
val frameSizeBytes =
|
|
165
172
|
(DeviceStore.store.get("bluetooth", "lc3_frame_size") as? Number)?.toInt()
|
|
166
173
|
?: DEFAULT_LC3_FRAME_SIZE_BYTES
|
|
@@ -173,13 +180,10 @@ public class Bridge private constructor() {
|
|
|
173
180
|
body["frameSizeBytes"] = frameSizeBytes
|
|
174
181
|
body["bitrate"] = frameSizeBytes * 8 * (1000 / LC3_FRAME_DURATION_MS)
|
|
175
182
|
body["packetizedFromGlasses"] = false
|
|
176
|
-
body["
|
|
183
|
+
body["voiceActivityDetectionEnabled"] = voiceActivityDetectionEnabled
|
|
177
184
|
return body
|
|
178
185
|
}
|
|
179
186
|
|
|
180
|
-
private fun isVadGated(): Boolean =
|
|
181
|
-
!((DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean) ?: true)
|
|
182
|
-
|
|
183
187
|
/** Save a setting */
|
|
184
188
|
@JvmStatic
|
|
185
189
|
fun saveSetting(key: String, value: Any) {
|
|
@@ -189,12 +193,22 @@ public class Bridge private constructor() {
|
|
|
189
193
|
sendTypedMessage("save_setting", body as Map<String, Any>)
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
/** Send
|
|
196
|
+
/** Send Voice Activity Detection status */
|
|
193
197
|
@JvmStatic
|
|
194
|
-
fun
|
|
198
|
+
fun sendVoiceActivityDetectionStatus(enabled: Boolean) {
|
|
199
|
+
DeviceStore.set("glasses", "voiceActivityDetectionEnabled", enabled)
|
|
195
200
|
val body = HashMap<String, Any>()
|
|
196
|
-
body["
|
|
197
|
-
sendTypedMessage("
|
|
201
|
+
body["voiceActivityDetectionEnabled"] = enabled
|
|
202
|
+
sendTypedMessage("voice_activity_detection_status", body as Map<String, Any>)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Send live speaking status reported by glasses-side Voice Activity Detection. */
|
|
206
|
+
@JvmStatic
|
|
207
|
+
fun sendSpeakingStatus(speaking: Boolean) {
|
|
208
|
+
val body = HashMap<String, Any>()
|
|
209
|
+
body["speaking"] = speaking
|
|
210
|
+
body["timestamp"] = System.currentTimeMillis()
|
|
211
|
+
sendTypedMessage("speaking_status", body as Map<String, Any>)
|
|
198
212
|
}
|
|
199
213
|
|
|
200
214
|
/** Send battery status */
|
|
@@ -334,14 +348,29 @@ public class Bridge private constructor() {
|
|
|
334
348
|
|
|
335
349
|
@JvmStatic
|
|
336
350
|
fun sendPhotoError(requestId: String, errorCode: String, errorMessage: String) {
|
|
351
|
+
val timestamp = System.currentTimeMillis()
|
|
337
352
|
val event = HashMap<String, Any>()
|
|
338
353
|
event["type"] = "photo_response"
|
|
339
354
|
event["state"] = "error"
|
|
340
355
|
event["requestId"] = requestId
|
|
341
356
|
event["errorCode"] = errorCode
|
|
342
357
|
event["errorMessage"] = errorMessage
|
|
343
|
-
event["timestamp"] =
|
|
358
|
+
event["timestamp"] = timestamp
|
|
344
359
|
sendTypedMessage("photo_response", event as Map<String, Any>)
|
|
360
|
+
|
|
361
|
+
val status = HashMap<String, Any>()
|
|
362
|
+
status["type"] = "photo_status"
|
|
363
|
+
status["status"] = "failed"
|
|
364
|
+
status["requestId"] = requestId
|
|
365
|
+
status["errorCode"] = errorCode
|
|
366
|
+
status["errorMessage"] = errorMessage
|
|
367
|
+
status["timestamp"] = timestamp
|
|
368
|
+
sendTypedMessage("photo_status", status as Map<String, Any>)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@JvmStatic
|
|
372
|
+
fun sendPhotoStatus(statusJson: Map<String, Any>) {
|
|
373
|
+
sendTypedMessage("photo_status", statusJson)
|
|
345
374
|
}
|
|
346
375
|
|
|
347
376
|
/** Send RGB LED control response */
|
|
@@ -632,6 +661,15 @@ public class Bridge private constructor() {
|
|
|
632
661
|
return
|
|
633
662
|
}
|
|
634
663
|
|
|
664
|
+
if (type != "log") {
|
|
665
|
+
BleTraceLogger.logMap(
|
|
666
|
+
"phone_to_app",
|
|
667
|
+
"sdk_event_dispatch",
|
|
668
|
+
type,
|
|
669
|
+
mutableBody as Map<String, Any>,
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
|
|
635
673
|
// Send directly using type as event name - no JSON serialization
|
|
636
674
|
sinks.forEach { sink ->
|
|
637
675
|
try {
|
|
@@ -132,10 +132,6 @@ class DeviceManager {
|
|
|
132
132
|
get() = DeviceStore.store.get("bluetooth", "power_saving_mode") as? Boolean ?: false
|
|
133
133
|
set(value) = DeviceStore.apply("bluetooth", "power_saving_mode", value)
|
|
134
134
|
|
|
135
|
-
private var bypassVad: Boolean
|
|
136
|
-
get() = DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean ?: true
|
|
137
|
-
set(value) = DeviceStore.apply("bluetooth", "bypass_vad", value)
|
|
138
|
-
|
|
139
135
|
private var offlineCaptionsRunning: Boolean
|
|
140
136
|
get() = DeviceStore.store.get("bluetooth", "offline_captions_running") as? Boolean ?: false
|
|
141
137
|
set(value) = DeviceStore.apply("bluetooth", "offline_captions_running", value)
|
|
@@ -169,8 +165,8 @@ class DeviceManager {
|
|
|
169
165
|
set(value) = DeviceStore.apply("bluetooth", "dashboard_depth", value)
|
|
170
166
|
|
|
171
167
|
private var galleryMode: Boolean
|
|
172
|
-
get() = DeviceStore.store.get("bluetooth", "
|
|
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,30 @@ 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,
|
|
1356
|
+
iso: Int? = null,
|
|
1384
1357
|
) {
|
|
1385
|
-
|
|
1386
|
-
|
|
1358
|
+
val exposureNs: Long? =
|
|
1359
|
+
exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
|
|
1360
|
+
when {
|
|
1361
|
+
v > Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
|
|
1362
|
+
else -> v.toLong()
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
val manualIso = if (exposureNs != null) iso?.takeIf { it > 0 } else null
|
|
1366
|
+
Bridge.log(
|
|
1367
|
+
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=$requestId appId=$appId size=$size compress=$compress flash=$flash sound=$sound exposureTimeNs=$exposureNs iso=${manualIso ?: "auto"} sgc=${sgc?.javaClass?.simpleName ?: "null"}"
|
|
1368
|
+
)
|
|
1369
|
+
val activeSgc = sgc
|
|
1370
|
+
if (activeSgc == null) {
|
|
1371
|
+
Bridge.log(
|
|
1372
|
+
"MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=$requestId"
|
|
1373
|
+
)
|
|
1374
|
+
return
|
|
1375
|
+
}
|
|
1376
|
+
activeSgc.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, sound, exposureNs, manualIso)
|
|
1387
1377
|
}
|
|
1388
1378
|
|
|
1389
1379
|
fun rgbLedControl(
|
|
@@ -1513,6 +1503,7 @@ class DeviceManager {
|
|
|
1513
1503
|
DeviceStore.apply("glasses", "deviceModel", "")
|
|
1514
1504
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
1515
1505
|
DeviceStore.apply("glasses", "connected", false)
|
|
1506
|
+
DeviceStore.apply("glasses", "voiceActivityDetectionEnabled", true)
|
|
1516
1507
|
// disconnect the controller as well:
|
|
1517
1508
|
searchingController = false
|
|
1518
1509
|
DeviceStore.apply("glasses", "controllerConnected", false)
|