@mentra/bluetooth-sdk 0.1.7 → 0.1.9
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 +11 -9
- package/android/.project +28 -0
- package/android/build.gradle +3 -0
- package/android/lc3Lib/.project +28 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +15 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +26 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +4 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +136 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +40 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +1 -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 +1 -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 +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +104 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +128 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +105 -0
- package/build/BluetoothSdk.types.d.ts +66 -29
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +3 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +2 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +24 -7
- 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 +2 -3
- 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/expo-module.config.json +5 -2
- package/ios/BluetoothSdkModule.swift +19 -3
- package/ios/LocalPhotoUploadServer.swift +411 -0
- package/ios/MentraBluetoothSDK.podspec +8 -2
- package/ios/MentraPhotoReceiverModule.swift +147 -0
- package/ios/Source/Bridge.swift +20 -1
- package/ios/Source/Camera/CameraModels.swift +64 -11
- package/ios/Source/DeviceManager.swift +6 -4
- package/ios/Source/Events/BluetoothEvents.swift +3 -0
- package/ios/Source/MentraBluetoothSDK.swift +111 -5
- package/ios/Source/ObservableStore.swift +2 -2
- package/ios/Source/Streaming/StreamModels.swift +234 -40
- package/ios/Source/controllers/ControllerManager.swift +1 -1
- package/ios/Source/controllers/R1.swift +9 -3
- package/ios/Source/sgcs/G1.swift +5 -4
- package/ios/Source/sgcs/G2.swift +4 -4
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +76 -12
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +1 -1
- package/ios/Source/sgcs/Simulated.swift +1 -1
- package/ios/Source/utils/MessageChunkReassembler.swift +89 -0
- package/package.json +6 -1
- package/src/BluetoothSdk.types.ts +90 -35
- package/src/_private/BluetoothSdkModule.ts +38 -8
- package/src/index.ts +9 -4
- 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/README.md
CHANGED
|
@@ -266,11 +266,17 @@ await BluetoothSdk.requestPhoto({
|
|
|
266
266
|
authToken: 'optional-token',
|
|
267
267
|
compress: 'medium',
|
|
268
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
|
|
269
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
|
|
@@ -283,16 +289,11 @@ await BluetoothSdk.startStream({
|
|
|
283
289
|
video: {fps: 15},
|
|
284
290
|
})
|
|
285
291
|
|
|
286
|
-
await BluetoothSdk.keepStreamAlive({
|
|
287
|
-
type: 'keep_stream_alive',
|
|
288
|
-
streamId,
|
|
289
|
-
ackId: `ack-${Date.now()}`,
|
|
290
|
-
})
|
|
291
|
-
|
|
292
292
|
await BluetoothSdk.stopStream()
|
|
293
293
|
```
|
|
294
294
|
|
|
295
|
-
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.
|
|
296
297
|
|
|
297
298
|
## Events
|
|
298
299
|
|
|
@@ -304,6 +305,7 @@ import {useBluetoothEvent} from '@mentra/bluetooth-sdk/react'
|
|
|
304
305
|
export function HardwareEventLogger() {
|
|
305
306
|
useBluetoothEvent('button_press', (event) => console.log(event))
|
|
306
307
|
useBluetoothEvent('touch_event', (event) => console.log(event))
|
|
308
|
+
useBluetoothEvent('photo_status', (event) => console.log(event.status, event.resolvedConfig))
|
|
307
309
|
useBluetoothEvent('photo_response', (event) => console.log(event))
|
|
308
310
|
useBluetoothEvent('stream_status', (event) => console.log(event))
|
|
309
311
|
useBluetoothEvent('speaking_status', (event) => console.log(event.speaking))
|
|
@@ -318,9 +320,9 @@ export function HardwareEventLogger() {
|
|
|
318
320
|
|
|
319
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.
|
|
320
322
|
|
|
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`, `
|
|
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`.
|
|
322
324
|
|
|
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`.
|
|
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`.
|
|
324
326
|
|
|
325
327
|
Only documented imports are supported for app developers. Undocumented package subpaths or symbols with a leading underscore can change without notice.
|
|
326
328
|
|
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,6 +83,9 @@ 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
|
|
|
@@ -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
|
|
@@ -108,6 +109,10 @@ class BluetoothSdkModule : Module() {
|
|
|
108
109
|
sendEvent("photo_response", event.values)
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
override fun onPhotoStatus(event: PhotoStatusEvent) {
|
|
113
|
+
sendEvent("photo_status", event.values)
|
|
114
|
+
}
|
|
115
|
+
|
|
111
116
|
override fun onStreamStatus(event: StreamStatusEvent) {
|
|
112
117
|
sendEvent("stream_status", event.values)
|
|
113
118
|
}
|
|
@@ -164,6 +169,7 @@ class BluetoothSdkModule : Module() {
|
|
|
164
169
|
"hotspot_status_change",
|
|
165
170
|
"hotspot_error",
|
|
166
171
|
"photo_response",
|
|
172
|
+
"photo_status",
|
|
167
173
|
"gallery_status",
|
|
168
174
|
"compatible_glasses_search_stop",
|
|
169
175
|
"heartbeat_sent",
|
|
@@ -203,11 +209,17 @@ class BluetoothSdkModule : Module() {
|
|
|
203
209
|
appContext.reactContext
|
|
204
210
|
?: appContext.currentActivity
|
|
205
211
|
?: throw IllegalStateException("No context available")
|
|
212
|
+
BleTraceLogger.logLifecycle(context, "BluetoothSdkModule", "module_create")
|
|
206
213
|
sdk = MentraBluetoothSdk.create(context, sdkListener)
|
|
207
214
|
deviceManager = DeviceManager.getInstance()
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
OnDestroy {
|
|
218
|
+
BleTraceLogger.logLifecycle(
|
|
219
|
+
appContext.reactContext ?: appContext.currentActivity,
|
|
220
|
+
"BluetoothSdkModule",
|
|
221
|
+
"module_destroy"
|
|
222
|
+
)
|
|
211
223
|
sdk?.close()
|
|
212
224
|
sdk = null
|
|
213
225
|
deviceManager = null
|
|
@@ -376,7 +388,7 @@ class BluetoothSdkModule : Module() {
|
|
|
376
388
|
}.toMap()
|
|
377
389
|
val req = PhotoRequest.fromMap(sanitized)
|
|
378
390
|
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}"
|
|
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}"
|
|
380
392
|
)
|
|
381
393
|
val activeSdk = sdk
|
|
382
394
|
if (activeSdk == null) {
|
|
@@ -422,8 +434,8 @@ class BluetoothSdkModule : Module() {
|
|
|
422
434
|
|
|
423
435
|
AsyncFunction("stopStream") { sdk?.stopStream() }
|
|
424
436
|
|
|
425
|
-
AsyncFunction("
|
|
426
|
-
sdk?.
|
|
437
|
+
AsyncFunction("sendCloudStreamKeepAlive") { params: Map<String, Any> ->
|
|
438
|
+
sdk?.sendCloudStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
|
|
427
439
|
}
|
|
428
440
|
|
|
429
441
|
// MARK: - Microphone Commands
|
|
@@ -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
|
|
@@ -347,14 +348,29 @@ public class Bridge private constructor() {
|
|
|
347
348
|
|
|
348
349
|
@JvmStatic
|
|
349
350
|
fun sendPhotoError(requestId: String, errorCode: String, errorMessage: String) {
|
|
351
|
+
val timestamp = System.currentTimeMillis()
|
|
350
352
|
val event = HashMap<String, Any>()
|
|
351
353
|
event["type"] = "photo_response"
|
|
352
354
|
event["state"] = "error"
|
|
353
355
|
event["requestId"] = requestId
|
|
354
356
|
event["errorCode"] = errorCode
|
|
355
357
|
event["errorMessage"] = errorMessage
|
|
356
|
-
event["timestamp"] =
|
|
358
|
+
event["timestamp"] = timestamp
|
|
357
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)
|
|
358
374
|
}
|
|
359
375
|
|
|
360
376
|
/** Send RGB LED control response */
|
|
@@ -645,6 +661,15 @@ public class Bridge private constructor() {
|
|
|
645
661
|
return
|
|
646
662
|
}
|
|
647
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
|
+
|
|
648
673
|
// Send directly using type as event name - no JSON serialization
|
|
649
674
|
sinks.forEach { sink ->
|
|
650
675
|
try {
|
|
@@ -1353,6 +1353,7 @@ class DeviceManager {
|
|
|
1353
1353
|
flash: Boolean,
|
|
1354
1354
|
sound: Boolean,
|
|
1355
1355
|
exposureTimeNs: Double? = null,
|
|
1356
|
+
iso: Int? = null,
|
|
1356
1357
|
) {
|
|
1357
1358
|
val exposureNs: Long? =
|
|
1358
1359
|
exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
|
|
@@ -1361,8 +1362,9 @@ class DeviceManager {
|
|
|
1361
1362
|
else -> v.toLong()
|
|
1362
1363
|
}
|
|
1363
1364
|
}
|
|
1365
|
+
val manualIso = if (exposureNs != null) iso?.takeIf { it > 0 } else null
|
|
1364
1366
|
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"}"
|
|
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"}"
|
|
1366
1368
|
)
|
|
1367
1369
|
val activeSgc = sgc
|
|
1368
1370
|
if (activeSgc == null) {
|
|
@@ -1371,7 +1373,7 @@ class DeviceManager {
|
|
|
1371
1373
|
)
|
|
1372
1374
|
return
|
|
1373
1375
|
}
|
|
1374
|
-
activeSgc.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, sound, exposureNs)
|
|
1376
|
+
activeSgc.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, sound, exposureNs, manualIso)
|
|
1375
1377
|
}
|
|
1376
1378
|
|
|
1377
1379
|
fun rgbLedControl(
|
|
@@ -7,6 +7,7 @@ import android.os.Looper
|
|
|
7
7
|
import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
8
8
|
import com.mentra.bluetoothsdk.utils.PhoneAudioMonitor
|
|
9
9
|
import java.util.Collections
|
|
10
|
+
import java.util.UUID
|
|
10
11
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
12
|
|
|
12
13
|
class MentraBluetoothSdk private constructor(
|
|
@@ -23,6 +24,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
23
24
|
private val bridgeEventSinkId: String
|
|
24
25
|
private val storeListenerId: String
|
|
25
26
|
private var suppressDefaultDeviceEvents = false
|
|
27
|
+
private val streamKeepAliveLock = Any()
|
|
28
|
+
private var activeStreamKeepAlive: ActiveStreamKeepAlive? = null
|
|
26
29
|
|
|
27
30
|
init {
|
|
28
31
|
listeners.add(listener)
|
|
@@ -36,6 +39,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
36
39
|
private val DEFAULT_DEVICE_KEYS = setOf("default_wearable", "device_name", "device_address")
|
|
37
40
|
private val SCAN_STATE_KEYS = setOf("searching", "searchingController", "searchResults")
|
|
38
41
|
private const val DEFAULT_SCAN_TIMEOUT_MS = 15_000L
|
|
42
|
+
private const val DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS = 5
|
|
43
|
+
private const val MAX_MISSED_STREAM_KEEP_ALIVE_ACKS = 3
|
|
39
44
|
|
|
40
45
|
@JvmStatic
|
|
41
46
|
fun create(
|
|
@@ -51,6 +56,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
51
56
|
): MentraBluetoothSdk = MentraBluetoothSdk(context, config, listener)
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
private data class ActiveStreamKeepAlive(
|
|
60
|
+
val streamId: String,
|
|
61
|
+
val intervalMs: Long,
|
|
62
|
+
var pendingAckId: String? = null,
|
|
63
|
+
var missedAckCount: Int = 0,
|
|
64
|
+
var nextTick: Runnable? = null,
|
|
65
|
+
)
|
|
66
|
+
|
|
54
67
|
fun addListener(listener: MentraBluetoothSdkListener) {
|
|
55
68
|
listeners.add(listener)
|
|
56
69
|
}
|
|
@@ -427,6 +440,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
427
440
|
request.flash,
|
|
428
441
|
request.sound,
|
|
429
442
|
request.exposureTimeNs,
|
|
443
|
+
request.iso,
|
|
430
444
|
)
|
|
431
445
|
}
|
|
432
446
|
|
|
@@ -435,10 +449,18 @@ class MentraBluetoothSdk private constructor(
|
|
|
435
449
|
}
|
|
436
450
|
|
|
437
451
|
fun startStream(request: StreamRequest) {
|
|
438
|
-
|
|
452
|
+
val message = request.toMap().toMutableMap()
|
|
453
|
+
val streamId = (message["streamId"] as? String)?.takeIf { it.isNotBlank() }
|
|
454
|
+
?: "sdk-${UUID.randomUUID()}"
|
|
455
|
+
message["streamId"] = streamId
|
|
456
|
+
stopStreamKeepAliveMonitor()
|
|
457
|
+
deviceManager.startStream(message)
|
|
458
|
+
if (request.keepAlive && !request.isExternallyManagedKeepAlive()) {
|
|
459
|
+
startStreamKeepAliveMonitor(streamId, request.keepAliveIntervalSeconds)
|
|
460
|
+
}
|
|
439
461
|
}
|
|
440
462
|
|
|
441
|
-
fun
|
|
463
|
+
internal fun sendCloudStreamKeepAlive(request: StreamKeepAliveRequest) {
|
|
442
464
|
deviceManager.keepStreamAlive(request.toMap().toMutableMap())
|
|
443
465
|
}
|
|
444
466
|
|
|
@@ -455,6 +477,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
455
477
|
}
|
|
456
478
|
|
|
457
479
|
fun stopStream() {
|
|
480
|
+
stopStreamKeepAliveMonitor()
|
|
458
481
|
deviceManager.stopStream()
|
|
459
482
|
}
|
|
460
483
|
|
|
@@ -491,6 +514,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
491
514
|
}
|
|
492
515
|
|
|
493
516
|
override fun close() {
|
|
517
|
+
stopStreamKeepAliveMonitor()
|
|
494
518
|
Bridge.removeEventSink(bridgeEventSinkId)
|
|
495
519
|
DeviceStore.store.removeListener(storeListenerId)
|
|
496
520
|
listeners.clear()
|
|
@@ -644,8 +668,18 @@ class MentraBluetoothSdk private constructor(
|
|
|
644
668
|
"hotspot_error" -> dispatchToListeners { it.onHotspotError(HotspotErrorEvent(data)) }
|
|
645
669
|
"gallery_status" -> dispatchToListeners { it.onGalleryStatus(GalleryStatusEvent(data)) }
|
|
646
670
|
"photo_response" -> dispatchToListeners { it.onPhotoResponse(PhotoResponseEvent(data)) }
|
|
647
|
-
"
|
|
648
|
-
"
|
|
671
|
+
"photo_status" -> dispatchToListeners { it.onPhotoStatus(PhotoStatusEvent(data)) }
|
|
672
|
+
"stream_status" -> {
|
|
673
|
+
val event = StreamStatusEvent(data)
|
|
674
|
+
handleStreamStatusForKeepAlive(event.status)
|
|
675
|
+
dispatchToListeners { it.onStreamStatus(event) }
|
|
676
|
+
}
|
|
677
|
+
"keep_alive_ack" -> {
|
|
678
|
+
val event = KeepAliveAckEvent(data)
|
|
679
|
+
if (!handleStreamKeepAliveAck(event)) {
|
|
680
|
+
dispatchToListeners { it.onKeepAliveAck(event) }
|
|
681
|
+
}
|
|
682
|
+
}
|
|
649
683
|
"mic_pcm" -> {
|
|
650
684
|
val event = MicPcmEvent(data)
|
|
651
685
|
if (event.pcm.isNotEmpty()) {
|
|
@@ -700,4 +734,102 @@ class MentraBluetoothSdk private constructor(
|
|
|
700
734
|
deliver()
|
|
701
735
|
}
|
|
702
736
|
}
|
|
737
|
+
|
|
738
|
+
private fun startStreamKeepAliveMonitor(streamId: String, requestedIntervalSeconds: Int) {
|
|
739
|
+
val intervalSeconds =
|
|
740
|
+
requestedIntervalSeconds.takeIf { it > 0 } ?: DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS
|
|
741
|
+
val tracker = ActiveStreamKeepAlive(
|
|
742
|
+
streamId = streamId,
|
|
743
|
+
intervalMs = intervalSeconds * 1_000L,
|
|
744
|
+
)
|
|
745
|
+
synchronized(streamKeepAliveLock) {
|
|
746
|
+
activeStreamKeepAlive = tracker
|
|
747
|
+
}
|
|
748
|
+
sendNextStreamKeepAlive(tracker)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private fun stopStreamKeepAliveMonitor() {
|
|
752
|
+
val tracker =
|
|
753
|
+
synchronized(streamKeepAliveLock) {
|
|
754
|
+
val current = activeStreamKeepAlive
|
|
755
|
+
activeStreamKeepAlive = null
|
|
756
|
+
current
|
|
757
|
+
}
|
|
758
|
+
tracker?.nextTick?.let { mainHandler.removeCallbacks(it) }
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private fun sendNextStreamKeepAlive(tracker: ActiveStreamKeepAlive) {
|
|
762
|
+
var timeoutEvent: StreamStatusEvent? = null
|
|
763
|
+
var request: StreamKeepAliveRequest? = null
|
|
764
|
+
|
|
765
|
+
synchronized(streamKeepAliveLock) {
|
|
766
|
+
if (activeStreamKeepAlive !== tracker) {
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (tracker.pendingAckId != null) {
|
|
771
|
+
tracker.missedAckCount += 1
|
|
772
|
+
if (tracker.missedAckCount >= MAX_MISSED_STREAM_KEEP_ALIVE_ACKS) {
|
|
773
|
+
activeStreamKeepAlive = null
|
|
774
|
+
tracker.nextTick?.let { mainHandler.removeCallbacks(it) }
|
|
775
|
+
timeoutEvent =
|
|
776
|
+
StreamStatusEvent(
|
|
777
|
+
StreamStatus.Error(
|
|
778
|
+
streamId = tracker.streamId,
|
|
779
|
+
errorDetails =
|
|
780
|
+
"Stream keep-alive timed out after ${tracker.missedAckCount} missed ACKs",
|
|
781
|
+
timestamp = System.currentTimeMillis(),
|
|
782
|
+
resolvedConfig = null,
|
|
783
|
+
)
|
|
784
|
+
)
|
|
785
|
+
return@synchronized
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
val ackId = "ack-${System.currentTimeMillis()}"
|
|
790
|
+
tracker.pendingAckId = ackId
|
|
791
|
+
request = StreamKeepAliveRequest(streamId = tracker.streamId, ackId = ackId)
|
|
792
|
+
val nextTick = Runnable { sendNextStreamKeepAlive(tracker) }
|
|
793
|
+
tracker.nextTick = nextTick
|
|
794
|
+
mainHandler.postDelayed(nextTick, tracker.intervalMs)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
timeoutEvent?.let { event ->
|
|
798
|
+
dispatchToListeners { it.onStreamStatus(event) }
|
|
799
|
+
return
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
request?.let { keepAlive ->
|
|
803
|
+
deviceManager.keepStreamAlive(keepAlive.toMap().toMutableMap())
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private fun handleStreamKeepAliveAck(event: KeepAliveAckEvent): Boolean {
|
|
808
|
+
synchronized(streamKeepAliveLock) {
|
|
809
|
+
val tracker = activeStreamKeepAlive ?: return false
|
|
810
|
+
if (event.streamId != tracker.streamId || event.ackId != tracker.pendingAckId) {
|
|
811
|
+
return false
|
|
812
|
+
}
|
|
813
|
+
tracker.pendingAckId = null
|
|
814
|
+
tracker.missedAckCount = 0
|
|
815
|
+
return true
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private fun handleStreamStatusForKeepAlive(status: StreamStatus) {
|
|
820
|
+
val streamId = status.streamId
|
|
821
|
+
if (
|
|
822
|
+
streamId != null &&
|
|
823
|
+
synchronized(streamKeepAliveLock) { activeStreamKeepAlive?.streamId } != streamId
|
|
824
|
+
) {
|
|
825
|
+
return
|
|
826
|
+
}
|
|
827
|
+
when (status.state) {
|
|
828
|
+
StreamState.STOPPED,
|
|
829
|
+
StreamState.STOPPING,
|
|
830
|
+
StreamState.ERROR,
|
|
831
|
+
StreamState.RECONNECT_FAILED -> stopStreamKeepAliveMonitor()
|
|
832
|
+
else -> Unit
|
|
833
|
+
}
|
|
834
|
+
}
|
|
703
835
|
}
|
|
@@ -47,9 +47,21 @@ data class ButtonVideoRecordingSettings(
|
|
|
47
47
|
val fps: Int,
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
class CameraFov @JvmOverloads constructor(
|
|
51
|
+
fov: Int = DEFAULT_FOV,
|
|
52
|
+
roiPosition: Int = DEFAULT_ROI_POSITION,
|
|
53
|
+
) {
|
|
54
|
+
val fov: Int = fov.coerceIn(MIN_FOV, MAX_FOV)
|
|
55
|
+
val roiPosition: Int = roiPosition.coerceIn(MIN_ROI_POSITION, MAX_ROI_POSITION)
|
|
56
|
+
|
|
57
|
+
companion object {
|
|
58
|
+
const val MIN_FOV = 82
|
|
59
|
+
const val MAX_FOV = 118
|
|
60
|
+
const val DEFAULT_FOV = 102
|
|
61
|
+
const val MIN_ROI_POSITION = 0
|
|
62
|
+
const val MAX_ROI_POSITION = 2
|
|
63
|
+
const val DEFAULT_ROI_POSITION = 0
|
|
64
|
+
}
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
data class PhotoRequest @JvmOverloads constructor(
|
|
@@ -63,6 +75,8 @@ data class PhotoRequest @JvmOverloads constructor(
|
|
|
63
75
|
val sound: Boolean = true,
|
|
64
76
|
/** Sensor exposure time for this capture only (ns), or null for auto exposure */
|
|
65
77
|
val exposureTimeNs: Double? = null,
|
|
78
|
+
/** Sensor ISO for this capture only. Only used when exposureTimeNs enables manual exposure. */
|
|
79
|
+
val iso: Int? = null,
|
|
66
80
|
) {
|
|
67
81
|
companion object {
|
|
68
82
|
/** Mirrors iOS `BluetoothSdkModule` defaults for keys omitted from the JS bridge. */
|
|
@@ -74,6 +88,17 @@ data class PhotoRequest @JvmOverloads constructor(
|
|
|
74
88
|
is Number -> rawExp.toDouble().takeIf { it.isFinite() && it > 0 }
|
|
75
89
|
else -> null
|
|
76
90
|
}
|
|
91
|
+
val rawIso = values["iso"]
|
|
92
|
+
val iso: Int? =
|
|
93
|
+
when (rawIso) {
|
|
94
|
+
is Number -> rawIso.toDouble().takeIf { it.isFinite() && it > 0 }?.let { value ->
|
|
95
|
+
when {
|
|
96
|
+
value > Int.MAX_VALUE.toDouble() -> Int.MAX_VALUE
|
|
97
|
+
else -> value.toInt()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else -> null
|
|
101
|
+
}
|
|
77
102
|
return PhotoRequest(
|
|
78
103
|
requestId = stringValue(values, "requestId", "request_id").orEmpty(),
|
|
79
104
|
appId = stringValue(values, "appId", "app_id").orEmpty(),
|
|
@@ -84,6 +109,7 @@ data class PhotoRequest @JvmOverloads constructor(
|
|
|
84
109
|
flash = boolValue(values, "flash") ?: true,
|
|
85
110
|
sound = boolValue(values, "sound") ?: true,
|
|
86
111
|
exposureTimeNs = exposureTimeNs,
|
|
112
|
+
iso = iso,
|
|
87
113
|
)
|
|
88
114
|
}
|
|
89
115
|
}
|
|
@@ -207,3 +233,14 @@ data class PhotoResponseEvent(
|
|
|
207
233
|
val requestId: String get() = response.requestId
|
|
208
234
|
val values: Map<String, Any> get() = response.toEventMap()
|
|
209
235
|
}
|
|
236
|
+
|
|
237
|
+
data class PhotoStatusEvent(
|
|
238
|
+
val values: Map<String, Any>,
|
|
239
|
+
) {
|
|
240
|
+
val requestId: String get() = stringValue(values, "requestId").orEmpty()
|
|
241
|
+
val status: String get() = stringValue(values, "status").orEmpty()
|
|
242
|
+
val timestamp: Long get() = longValue(values, "timestamp") ?: System.currentTimeMillis()
|
|
243
|
+
val resolvedConfig: Map<String, Any>? get() = stringMapValue(values["resolvedConfig"])
|
|
244
|
+
val errorCode: String? get() = stringValue(values, "errorCode")
|
|
245
|
+
val errorMessage: String? get() = stringValue(values, "errorMessage")
|
|
246
|
+
}
|
|
@@ -864,7 +864,7 @@ class R1 : ControllerManager() {
|
|
|
864
864
|
override fun requestPhoto(
|
|
865
865
|
requestId: String, appId: String, size: String?, webhookUrl: String?,
|
|
866
866
|
authToken: String?, compress: String?, flash: Boolean, sound: Boolean,
|
|
867
|
-
exposureTimeNs: Long?,
|
|
867
|
+
exposureTimeNs: Long?, iso: Int?,
|
|
868
868
|
) {}
|
|
869
869
|
override fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {}
|
|
870
870
|
override fun stopVideoRecording(requestId: String) {}
|