@mentra/bluetooth-sdk 0.1.20 → 0.1.21-beta.0
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 +5 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +127 -24
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +65 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +46 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +369 -97
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +183 -30
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +4 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +15 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +15 -0
- package/build/BluetoothSdk.types.d.ts +46 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +23 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +20 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
- package/build/_private/MentraLocalNetworkModule.js +3 -0
- package/build/_private/MentraLocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +1 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +1 -0
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/expo-module.config.json +1 -0
- package/ios/BluetoothSdkModule.swift +72 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +72 -18
- package/ios/Source/DeviceManager.swift +51 -3
- package/ios/Source/DeviceStore.swift +1 -1
- package/ios/Source/MentraBluetoothSDK.swift +289 -25
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +8 -3
- package/ios/Source/sgcs/G1.swift +1 -1
- package/ios/Source/sgcs/G2.swift +19 -7
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +318 -37
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/Nimo.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +5 -2
- package/ios/Source/sgcs/Simulated.swift +1 -1
- package/ios/Source/status/WifiHotspotStatus.swift +21 -2
- package/ios/Source/streaming/StreamModels.swift +9 -0
- package/ios/Source/tts/TTSTools.swift +1 -1
- package/package.json +1 -2
- package/src/BluetoothSdk.types.ts +49 -0
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +64 -13
- package/src/_private/MentraLocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +1 -0
- package/src/_private/photoRequestPayload.ts +2 -3
- package/src/index.ts +4 -0
package/README.md
CHANGED
|
@@ -380,12 +380,16 @@ const photo = await BluetoothSdk.requestPhoto({
|
|
|
380
380
|
console.log('photo delivered', photo.photoUrl ?? photo.uploadUrl, photo.fileSizeBytes)
|
|
381
381
|
```
|
|
382
382
|
|
|
383
|
-
`requestPhoto(...)` resolves only after the full photo action reaches terminal success: capture completed and the photo was delivered to the webhook, either directly from the glasses over Wi-Fi or through the phone's Bluetooth fallback relay. If you omit `requestId`, the SDK generates one and the terminal response includes it. It rejects if the ASG reports `state: "error"`, if phone-side fallback upload fails, if the SDK cannot send the command, or if no terminal `photo_response` arrives
|
|
383
|
+
`requestPhoto(...)` resolves only after the full photo action reaches terminal success: capture completed and the photo was delivered to the webhook, either directly from the glasses over Wi-Fi or through the phone's Bluetooth fallback relay. If you omit `requestId`, the SDK generates one and the terminal response includes it. It rejects if the ASG reports `state: "error"`, if phone-side fallback upload fails, if the SDK cannot send the command, or if no terminal `photo_response` arrives within 30 seconds. Photo requests use this longer operation-specific deadline because max-quality BLE fallback can legitimately exceed the 15-second deadline used by ordinary commands. Use `photo_status` for intermediate stages such as `accepted`, `configuring`, `capturing`, `captured`, `uploading`, `ble_fallback_compression`, `ready_for_transfer`, and `transferring`; `photo_status` is progress, while `photo_response` is terminal success/error. The raw `photo_response` event stream still includes both success and error events for subscribers. 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.
|
|
384
384
|
|
|
385
385
|
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`.
|
|
386
386
|
|
|
387
|
+
To own and explicitly release a warm camera, pass a request ID to `warmUpCamera({requestId, ...})`, then call `stopCameraWarmUp(requestId)` when the foreground UI closes. Warm holds default to 15 seconds and are capped at 60 seconds. Stopping while the camera is opening rejects the pending warm-up with `camera_warm_up_cancelled`; stopping after `ready` emits `stopped`. Compatible ready leases share the camera and expire independently.
|
|
388
|
+
|
|
387
389
|
Use `setCameraFov({fov, roiPosition})` to configure Mentra Live camera field of view and crop position. FOV is clamped to 62-118 degrees; ROI position is `"center"`, `"bottom"`, or `"top"`. You can also call `setCameraFov({preset: "narrow" | "standard" | "wide"})`; presets map to 82, 102, and 118 degrees with center ROI. The returned `CameraFovResult` resolves only after the ASG client reports that the setting was applied to camera hardware after the restart cooldown, and the promise rejects if the glasses report an error, persist the setting without hardware application, or time out. Raw `settings_ack` events remain available through `addListener("settings_ack", ...)` for diagnostic fields such as `hardwareApplied`. Treat FOV as a framing/ROI control; output resolution and effective detail can vary by capture path, firmware, and camera mode.
|
|
388
390
|
|
|
391
|
+
The missing/factory persistent FOV base is 102 degrees with centered ROI; existing saved values are not migrated. Use `setCameraFovOverride({leaseId, fov, roiPosition, ttlMs})` for a memory-only crop and `releaseCameraFovOverride(leaseId)` to restore the persistent base. A stale release is a safe no-op, and refreshing the same lease/config extends its TTL without another HAL restart.
|
|
392
|
+
|
|
389
393
|
`startVideoRecording(...)` resolves from the ASG `video_recording_status` event whose status is `recording_started`. `stopVideoRecording(...)` without a webhook resolves from `recording_stopped`; with a webhook it waits for `recording_stopped` plus a video `media_success`, and rejects on `media_error`. Raw `video_recording_status`, `media_success`, and `media_error` events remain available through listeners.
|
|
390
394
|
|
|
391
395
|
## Streaming
|
|
@@ -4,6 +4,7 @@ package com.mentra.bluetoothsdk
|
|
|
4
4
|
|
|
5
5
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
6
6
|
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
7
|
+
import com.mentra.bluetoothsdk.utils.audio.PcmStreamManager
|
|
7
8
|
import expo.modules.kotlin.exception.CodedException
|
|
8
9
|
import expo.modules.kotlin.functions.Coroutine
|
|
9
10
|
import expo.modules.kotlin.modules.Module
|
|
@@ -87,6 +88,45 @@ private inline fun <reified R> ModuleDefinitionBuilder.SdkCoroutineFunction(
|
|
|
87
88
|
crossinline body: suspend () -> R,
|
|
88
89
|
) = AsyncFunction(name) Coroutine { -> withExpoSdkErrorSuspend { body() } }
|
|
89
90
|
|
|
91
|
+
private inline fun <reified R, reified P0> ModuleDefinitionBuilder.SdkCoroutineFunction(
|
|
92
|
+
name: String,
|
|
93
|
+
crossinline body: suspend (P0) -> R,
|
|
94
|
+
) = AsyncFunction(name) Coroutine { p0: P0 -> withExpoSdkErrorSuspend { body(p0) } }
|
|
95
|
+
|
|
96
|
+
private inline fun <reified R, reified P0, reified P1> ModuleDefinitionBuilder.SdkCoroutineFunction(
|
|
97
|
+
name: String,
|
|
98
|
+
crossinline body: suspend (P0, P1) -> R,
|
|
99
|
+
) = AsyncFunction(name) Coroutine { p0: P0, p1: P1 -> withExpoSdkErrorSuspend { body(p0, p1) } }
|
|
100
|
+
|
|
101
|
+
private inline fun <reified R, reified P0, reified P1, reified P2> ModuleDefinitionBuilder.SdkCoroutineFunction(
|
|
102
|
+
name: String,
|
|
103
|
+
crossinline body: suspend (P0, P1, P2) -> R,
|
|
104
|
+
) = AsyncFunction(name) Coroutine { p0: P0, p1: P1, p2: P2 -> withExpoSdkErrorSuspend { body(p0, p1, p2) } }
|
|
105
|
+
|
|
106
|
+
private inline fun <reified R, reified P0, reified P1, reified P2, reified P3>
|
|
107
|
+
ModuleDefinitionBuilder.SdkCoroutineFunction(
|
|
108
|
+
name: String,
|
|
109
|
+
crossinline body: suspend (P0, P1, P2, P3) -> R,
|
|
110
|
+
) = AsyncFunction(name) Coroutine { p0: P0, p1: P1, p2: P2, p3: P3 ->
|
|
111
|
+
withExpoSdkErrorSuspend { body(p0, p1, p2, p3) }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private inline fun <
|
|
115
|
+
reified R,
|
|
116
|
+
reified P0,
|
|
117
|
+
reified P1,
|
|
118
|
+
reified P2,
|
|
119
|
+
reified P3,
|
|
120
|
+
reified P4,
|
|
121
|
+
reified P5,
|
|
122
|
+
reified P6,
|
|
123
|
+
> ModuleDefinitionBuilder.SdkCoroutineFunction(
|
|
124
|
+
name: String,
|
|
125
|
+
crossinline body: suspend (P0, P1, P2, P3, P4, P5, P6) -> R,
|
|
126
|
+
) = AsyncFunction(name) Coroutine { p0: P0, p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6 ->
|
|
127
|
+
withExpoSdkErrorSuspend { body(p0, p1, p2, p3, p4, p5, p6) }
|
|
128
|
+
}
|
|
129
|
+
|
|
90
130
|
class BluetoothSdkModule : Module() {
|
|
91
131
|
private var sdk: MentraBluetoothSdk? = null
|
|
92
132
|
private var deviceManager: DeviceManager? = null
|
|
@@ -360,6 +400,7 @@ class BluetoothSdkModule : Module() {
|
|
|
360
400
|
"module_destroy"
|
|
361
401
|
)
|
|
362
402
|
sdk?.close()
|
|
403
|
+
PcmStreamManager.abortAll()
|
|
363
404
|
sdk = null
|
|
364
405
|
deviceManager = null
|
|
365
406
|
}
|
|
@@ -496,17 +537,17 @@ class BluetoothSdkModule : Module() {
|
|
|
496
537
|
|
|
497
538
|
// MARK: - WiFi Commands
|
|
498
539
|
|
|
499
|
-
|
|
540
|
+
SdkCoroutineFunction("requestWifiScan") { -> requireSdk().requestWifiScan().map { it.toMap() } }
|
|
500
541
|
|
|
501
|
-
|
|
542
|
+
SdkCoroutineFunction("sendWifiCredentials") { ssid: String, password: String ->
|
|
502
543
|
requireSdk().sendWifiCredentials(ssid, password).values
|
|
503
544
|
}
|
|
504
545
|
|
|
505
|
-
|
|
546
|
+
SdkCoroutineFunction("forgetWifiNetwork") { ssid: String ->
|
|
506
547
|
requireSdk().forgetWifiNetwork(ssid).values
|
|
507
548
|
}
|
|
508
549
|
|
|
509
|
-
|
|
550
|
+
SdkCoroutineFunction("setHotspotState") { enabled: Boolean ->
|
|
510
551
|
requireSdk().setHotspotState(enabled).values
|
|
511
552
|
}
|
|
512
553
|
|
|
@@ -516,7 +557,7 @@ class BluetoothSdkModule : Module() {
|
|
|
516
557
|
|
|
517
558
|
// MARK: - Gallery Commands
|
|
518
559
|
|
|
519
|
-
|
|
560
|
+
SdkCoroutineFunction("setGalleryModeEnabled") { enabled: Boolean ->
|
|
520
561
|
requireSdk().setGalleryModeEnabled(enabled).values
|
|
521
562
|
}
|
|
522
563
|
|
|
@@ -525,19 +566,19 @@ class BluetoothSdkModule : Module() {
|
|
|
525
566
|
}
|
|
526
567
|
|
|
527
568
|
@Suppress("DEPRECATION")
|
|
528
|
-
|
|
569
|
+
SdkCoroutineFunction("setPhotoCaptureDefaults") { params: Map<String, Any?> ->
|
|
529
570
|
requireSdk().setPhotoCaptureDefaults(params.toPhotoCaptureDefaults()).values
|
|
530
571
|
}
|
|
531
572
|
|
|
532
|
-
|
|
573
|
+
SdkCoroutineFunction("setVideoRecordingDefaults") { width: Int, height: Int, fps: Int ->
|
|
533
574
|
requireSdk().setVideoRecordingDefaults(VideoRecordingDefaults(width, height, fps)).values
|
|
534
575
|
}
|
|
535
576
|
|
|
536
|
-
|
|
577
|
+
SdkCoroutineFunction("setMaxVideoRecordingDuration") { minutes: Int ->
|
|
537
578
|
requireSdk().setMaxVideoRecordingDuration(minutes).values
|
|
538
579
|
}
|
|
539
580
|
|
|
540
|
-
|
|
581
|
+
SdkCoroutineFunction("setCameraFov") { fov: Map<String, Any> ->
|
|
541
582
|
val value = (fov["fov"] as? Number)?.toInt() ?: CameraFov.DEFAULT_FOV
|
|
542
583
|
val roiPosition = CameraRoiPosition.fromValue(
|
|
543
584
|
(fov["roiPosition"] as? Number)?.toInt()
|
|
@@ -546,13 +587,41 @@ class BluetoothSdkModule : Module() {
|
|
|
546
587
|
requireSdk().setCameraFov(CameraFov(value, roiPosition)).values
|
|
547
588
|
}
|
|
548
589
|
|
|
549
|
-
|
|
590
|
+
SdkCoroutineFunction("setLegacyCameraFov") { fov: Map<String, Any> ->
|
|
591
|
+
val value = (fov["fov"] as? Number)?.toInt() ?: CameraFov.DEFAULT_FOV
|
|
592
|
+
val roiPosition = CameraRoiPosition.fromValue(
|
|
593
|
+
(fov["roiPosition"] as? Number)?.toInt()
|
|
594
|
+
?: (fov["roi_position"] as? Number)?.toInt(),
|
|
595
|
+
)
|
|
596
|
+
requireSdk().setLegacyCameraFov(CameraFov(value, roiPosition)).values
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
SdkCoroutineFunction("restoreLegacyCameraFov") { ->
|
|
600
|
+
requireSdk().restoreLegacyCameraFov()
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
SdkCoroutineFunction("setCameraFovOverride") { params: Map<String, Any> ->
|
|
604
|
+
val leaseId = params["leaseId"] as? String ?: throw IllegalArgumentException("leaseId is required")
|
|
605
|
+
val value = (params["fov"] as? Number)?.toInt() ?: CameraFov.DEFAULT_FOV
|
|
606
|
+
val roiPosition = CameraRoiPosition.fromValue(
|
|
607
|
+
(params["roiPosition"] as? Number)?.toInt()
|
|
608
|
+
?: (params["roi_position"] as? Number)?.toInt(),
|
|
609
|
+
)
|
|
610
|
+
val ttlMs = (params["ttlMs"] as? Number)?.toInt() ?: 300000
|
|
611
|
+
requireSdk().setCameraFovOverride(leaseId, CameraFov(value, roiPosition), ttlMs).values
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
SdkCoroutineFunction("releaseCameraFovOverride") { leaseId: String ->
|
|
615
|
+
requireSdk().releaseCameraFovOverride(leaseId).values
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
SdkCoroutineFunction("setCameraTuningConfig") { anrOn: Boolean, gainOn: Boolean ->
|
|
550
619
|
requireSdk().setCameraTuningConfig(anrOn, gainOn).values
|
|
551
620
|
}
|
|
552
621
|
|
|
553
|
-
|
|
622
|
+
SdkCoroutineFunction("queryGalleryStatus") { -> requireSdk().queryGalleryStatus().values }
|
|
554
623
|
|
|
555
|
-
|
|
624
|
+
SdkCoroutineFunction("requestPhoto") { params: Map<String, Any?> ->
|
|
556
625
|
// JS may pass null for optional fields; Map<String, Any> rejects null values at the bridge.
|
|
557
626
|
val sanitized =
|
|
558
627
|
params.mapNotNull { (key, value) ->
|
|
@@ -565,15 +634,20 @@ class BluetoothSdkModule : Module() {
|
|
|
565
634
|
requireSdk().requestPhoto(req).values
|
|
566
635
|
}
|
|
567
636
|
|
|
568
|
-
|
|
637
|
+
SdkCoroutineFunction("warmUpCamera") { params: Map<String, Any?> ->
|
|
569
638
|
val requestId = params["requestId"] as? String
|
|
570
639
|
val size = PhotoSize.fromValue(params["size"] as? String)
|
|
640
|
+
val mode = PhotoMode.fromValue(params["mode"] as? String)
|
|
571
641
|
val exposureRaw = (params["exposureTimeNs"] as? Number)?.toDouble()
|
|
572
642
|
val exposureTimeNs =
|
|
573
643
|
if (exposureRaw != null && exposureRaw.isFinite() && exposureRaw > 0) exposureRaw.toLong() else null
|
|
574
644
|
val durationRaw = (params["durationMs"] as? Number)?.toInt() ?: 0
|
|
575
645
|
val durationMs = if (durationRaw > 0) durationRaw else 15000
|
|
576
|
-
requireSdk().warmUpCamera(requestId, size, exposureTimeNs, durationMs).values
|
|
646
|
+
requireSdk().warmUpCamera(requestId, size, mode, exposureTimeNs, durationMs).values
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
SdkAsyncFunction("stopCameraWarmUp") { requestId: String ->
|
|
650
|
+
requireSdk().stopCameraWarmUp(requestId)
|
|
577
651
|
}
|
|
578
652
|
|
|
579
653
|
// MARK: - OTA Commands
|
|
@@ -586,11 +660,11 @@ class BluetoothSdkModule : Module() {
|
|
|
586
660
|
|
|
587
661
|
// Runs on Dispatchers.IO, not the shared Expo AsyncFunctionQueue:
|
|
588
662
|
// manifest fetches and version waits can block for several seconds.
|
|
589
|
-
SdkCoroutineFunction("checkForOtaUpdate") {
|
|
663
|
+
SdkCoroutineFunction("checkForOtaUpdate") { ->
|
|
590
664
|
withContext(Dispatchers.IO) { requireSdk().checkForOtaUpdate() }
|
|
591
665
|
}
|
|
592
666
|
|
|
593
|
-
|
|
667
|
+
SdkCoroutineFunction("startOtaUpdate") { otaVersionUrl: String? ->
|
|
594
668
|
val sdk = requireSdk()
|
|
595
669
|
if (otaVersionUrl.isNullOrBlank()) {
|
|
596
670
|
sdk.startOtaUpdate().values
|
|
@@ -599,11 +673,11 @@ class BluetoothSdkModule : Module() {
|
|
|
599
673
|
}
|
|
600
674
|
}
|
|
601
675
|
|
|
602
|
-
|
|
676
|
+
SdkCoroutineFunction("sendOtaQueryStatus") { -> requireSdk().sendOtaQueryStatus().values }
|
|
603
677
|
|
|
604
678
|
// MARK: - Version Info Commands
|
|
605
679
|
|
|
606
|
-
|
|
680
|
+
SdkCoroutineFunction("requestVersionInfo") { -> requireSdk().requestVersionInfo().toMap() }
|
|
607
681
|
|
|
608
682
|
// MARK: - Power Control Commands
|
|
609
683
|
|
|
@@ -613,7 +687,7 @@ class BluetoothSdkModule : Module() {
|
|
|
613
687
|
|
|
614
688
|
// MARK: - Video Recording Commands
|
|
615
689
|
|
|
616
|
-
|
|
690
|
+
SdkCoroutineFunction("startVideoRecording") {
|
|
617
691
|
requestId: String,
|
|
618
692
|
save: Boolean,
|
|
619
693
|
sound: Boolean,
|
|
@@ -637,7 +711,7 @@ class BluetoothSdkModule : Module() {
|
|
|
637
711
|
|
|
638
712
|
// webhookUrl/authToken are supplied at stop (not start) so the token is
|
|
639
713
|
// fresh when the upload runs. Empty/null webhook = keep on device.
|
|
640
|
-
|
|
714
|
+
SdkCoroutineFunction("stopVideoRecording") {
|
|
641
715
|
requestId: String,
|
|
642
716
|
webhookUrl: String?,
|
|
643
717
|
authToken: String? ->
|
|
@@ -646,15 +720,15 @@ class BluetoothSdkModule : Module() {
|
|
|
646
720
|
|
|
647
721
|
// MARK: - Stream Commands
|
|
648
722
|
|
|
649
|
-
|
|
723
|
+
SdkCoroutineFunction("startStream") { params: Map<String, Any> ->
|
|
650
724
|
requireSdk().startStream(StreamRequest.fromMap(params)).values
|
|
651
725
|
}
|
|
652
726
|
|
|
653
|
-
|
|
727
|
+
SdkCoroutineFunction("startExternallyManagedStream") { params: Map<String, Any> ->
|
|
654
728
|
requireSdk().startExternallyManagedStream(StreamRequest.fromMap(params)).values
|
|
655
729
|
}
|
|
656
730
|
|
|
657
|
-
|
|
731
|
+
SdkCoroutineFunction("stopStream") { -> requireSdk().stopStream().values }
|
|
658
732
|
|
|
659
733
|
SdkAsyncFunction("sendExternallyManagedStreamKeepAlive") { params: Map<String, Any> ->
|
|
660
734
|
sdk?.sendExternallyManagedStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
|
|
@@ -688,6 +762,35 @@ class BluetoothSdkModule : Module() {
|
|
|
688
762
|
sdk?.setOwnAppAudioPlaying(playing)
|
|
689
763
|
}
|
|
690
764
|
|
|
765
|
+
// MARK: - Live PCM output stream (miniapp speaker.createStream)
|
|
766
|
+
//
|
|
767
|
+
// MentraOS-internal. 16-bit LE PCM chunks into a streaming AudioTrack
|
|
768
|
+
// (USAGE_MEDIA → follows the media route, e.g. A2DP to glasses).
|
|
769
|
+
// write/close run *blocking on Dispatchers.IO*, not the shared Expo
|
|
770
|
+
// AsyncFunctionQueue: write intentionally blocks for backpressure and
|
|
771
|
+
// close blocks until the backlog drains — either would otherwise stall
|
|
772
|
+
// every other native call queued behind them.
|
|
773
|
+
|
|
774
|
+
AsyncFunction("pcmStreamOpen") { streamId: String, sampleRate: Int, channels: Int, volume: Double ->
|
|
775
|
+
PcmStreamManager.open(streamId, sampleRate, channels, volume.toFloat())
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
AsyncFunction("pcmStreamWrite") Coroutine { streamId: String, base64: String ->
|
|
779
|
+
withContext(Dispatchers.IO) {
|
|
780
|
+
mapOf("bufferedMs" to PcmStreamManager.write(streamId, base64))
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
AsyncFunction("pcmStreamClose") Coroutine { streamId: String ->
|
|
785
|
+
withContext(Dispatchers.IO) {
|
|
786
|
+
mapOf("durationMs" to PcmStreamManager.close(streamId))
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
AsyncFunction("pcmStreamAbort") { streamId: String ->
|
|
791
|
+
PcmStreamManager.abort(streamId)
|
|
792
|
+
}
|
|
793
|
+
|
|
691
794
|
// *Blocking on Dispatchers.IO, not the shared AsyncFunctionQueue: these wait on
|
|
692
795
|
// a CountDownLatch (up to 5s) for a BLE round-trip, which would otherwise stall
|
|
693
796
|
// every other native call queued behind them.
|
|
@@ -703,7 +806,7 @@ class BluetoothSdkModule : Module() {
|
|
|
703
806
|
|
|
704
807
|
// MARK: - RGB LED Control
|
|
705
808
|
|
|
706
|
-
|
|
809
|
+
SdkCoroutineFunction("rgbLedControl") {
|
|
707
810
|
requestId: String,
|
|
708
811
|
packageName: String?,
|
|
709
812
|
action: String,
|
|
@@ -520,30 +520,81 @@ public class Bridge private constructor() {
|
|
|
520
520
|
sendTypedMessage("glasses_serial_number", body as Map<String, Any>)
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
-
/**
|
|
523
|
+
/**
|
|
524
|
+
* Send WiFi status change. [error] is the glasses' provisioning failure reason
|
|
525
|
+
* (e.g. "connect_timeout", "connected_to_other_network") when this status is the
|
|
526
|
+
* verdict of a failed connect attempt; null for routine link-state updates.
|
|
527
|
+
*/
|
|
524
528
|
@JvmStatic
|
|
525
|
-
|
|
529
|
+
@JvmOverloads
|
|
530
|
+
fun sendWifiStatusChange(
|
|
531
|
+
connected: Boolean,
|
|
532
|
+
ssid: String?,
|
|
533
|
+
localIp: String?,
|
|
534
|
+
error: String? = null
|
|
535
|
+
) {
|
|
526
536
|
val status = WifiStatus.fromStoreFields(connected, ssid, localIp) ?: return
|
|
527
|
-
|
|
537
|
+
val payload =
|
|
538
|
+
if (error != null) status.toMap() + mapOf("error" to error)
|
|
539
|
+
else status.toMap()
|
|
540
|
+
sendTypedMessage("wifi_status_change", payload)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Claim the WiFi scan-results store for a newly requested scan. Called by the
|
|
545
|
+
* SDK when it generates the scanId, BEFORE the scan command goes out: store
|
|
546
|
+
* ownership is decided at request time, not by whichever chunk arrives first,
|
|
547
|
+
* so a delayed chunk from an older, abandoned scan can never reset or clobber
|
|
548
|
+
* the current scan's accumulator.
|
|
549
|
+
*/
|
|
550
|
+
@JvmStatic
|
|
551
|
+
fun claimWifiScanResults(scanId: String) {
|
|
552
|
+
DeviceStore.apply("bluetooth", "wifiScanActiveScanId", scanId)
|
|
528
553
|
}
|
|
529
554
|
|
|
530
555
|
/** Send WiFi scan results */
|
|
531
556
|
@JvmStatic
|
|
532
|
-
fun updateWifiScanResults(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
557
|
+
fun updateWifiScanResults(
|
|
558
|
+
networks: List<Map<String, Any>>,
|
|
559
|
+
scanComplete: Boolean,
|
|
560
|
+
scanId: String? = null
|
|
561
|
+
) {
|
|
562
|
+
// Only chunks echoing the active scanId claimed at request time may mutate
|
|
563
|
+
// the store; foreign chunks are still forwarded to the SDK sink, which
|
|
564
|
+
// drops stale ids itself. Scan-id-less chunks (old firmware) keep the
|
|
565
|
+
// legacy accumulate-forever store behavior.
|
|
566
|
+
val ownsStore =
|
|
567
|
+
scanId == null || scanId == DeviceStore.get("bluetooth", "wifiScanActiveScanId")
|
|
568
|
+
var updatedNetworks: List<Map<String, Any>> = networks
|
|
569
|
+
if (ownsStore) {
|
|
570
|
+
var storedNetworks: List<Map<String, Any>> =
|
|
571
|
+
DeviceStore.get("bluetooth", "wifiScanResults") as? List<Map<String, Any>>
|
|
572
|
+
?: emptyList()
|
|
573
|
+
val lastScanId = DeviceStore.get("bluetooth", "wifiScanResultsScanId")
|
|
574
|
+
if (scanId != null && scanId != lastScanId) {
|
|
575
|
+
// First chunk of a new scan: drop networks accumulated for a previous scan
|
|
576
|
+
// so stale entries never carry over into this scan's store.
|
|
577
|
+
storedNetworks = emptyList()
|
|
578
|
+
DeviceStore.apply("bluetooth", "wifiScanResultsScanId", scanId)
|
|
541
579
|
}
|
|
580
|
+
// add the networks to the storedNetworks array, removing duplicates by ssid
|
|
581
|
+
val merged = storedNetworks.toMutableList()
|
|
582
|
+
for (network in networks) {
|
|
583
|
+
if (!merged.any { it["ssid"] as? String == network["ssid"] as? String }) {
|
|
584
|
+
merged.add(network)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
DeviceStore.apply("bluetooth", "wifiScanResults", merged)
|
|
588
|
+
updatedNetworks = merged
|
|
542
589
|
}
|
|
543
|
-
DeviceStore.apply("bluetooth", "wifiScanResults", updatedNetworks)
|
|
544
590
|
val body = HashMap<String, Any>()
|
|
545
|
-
|
|
591
|
+
// Correlated scans: the SDK accumulates and dedupes chunks per scanId itself,
|
|
592
|
+
// so forward only this chunk; the merged store list is for UI consumers.
|
|
593
|
+
body["networks"] = if (scanId != null) networks else updatedNetworks
|
|
546
594
|
body["scanComplete"] = scanComplete
|
|
595
|
+
if (scanId != null) {
|
|
596
|
+
body["scanId"] = scanId
|
|
597
|
+
}
|
|
547
598
|
sendTypedMessage("wifi_scan_result", body)
|
|
548
599
|
}
|
|
549
600
|
|
|
@@ -329,6 +329,11 @@ class DeviceManager {
|
|
|
329
329
|
return
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
if (sgc?.isMicSuspendedForAudio == true) {
|
|
333
|
+
Bridge.log("MAN: Glasses mic intentionally suspended for phone audio; skipping mic recovery")
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
332
337
|
// When no frame has ever been received, treat elapsed as "forever" so we
|
|
333
338
|
// actually attempt recovery (was 0 before, which made the watchdog a no-op).
|
|
334
339
|
val timeSinceLastLc3Event = System.currentTimeMillis() - (lastLc3Event ?: 0L)
|
|
@@ -1556,10 +1561,10 @@ class DeviceManager {
|
|
|
1556
1561
|
sgc?.sendStreamKeepAlive(message)
|
|
1557
1562
|
}
|
|
1558
1563
|
|
|
1559
|
-
fun requestWifiScan() {
|
|
1564
|
+
fun requestWifiScan(scanId: String? = null) {
|
|
1560
1565
|
Bridge.log("MAN: Requesting wifi scan")
|
|
1561
1566
|
DeviceStore.apply("bluetooth", "wifiScanResults", emptyList<Any>())
|
|
1562
|
-
sgc?.requestWifiScan()
|
|
1567
|
+
sgc?.requestWifiScan(scanId)
|
|
1563
1568
|
}
|
|
1564
1569
|
|
|
1565
1570
|
fun sendIncidentId(incidentId: String, apiBaseUrl: String? = null) {
|
|
@@ -1649,6 +1654,38 @@ class DeviceManager {
|
|
|
1649
1654
|
live.sendCameraFovSetting(requestId, fov, roiPosition)
|
|
1650
1655
|
}
|
|
1651
1656
|
|
|
1657
|
+
/** Sends the pre-lease FOV command used by ASG clients that do not send an acknowledgement. */
|
|
1658
|
+
fun sendLegacyCameraFovSetting(fov: Int, roiPosition: Int) {
|
|
1659
|
+
val glassesConnected = DeviceStore.get("glasses", "connected") as? Boolean ?: false
|
|
1660
|
+
if (!glassesConnected) throw IllegalStateException("not_connected")
|
|
1661
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1662
|
+
live.sendCameraFovSetting(null, fov, roiPosition)
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/** Replays the persistent base FOV without waiting for an acknowledgement. */
|
|
1666
|
+
fun restoreLegacyCameraFovSetting() {
|
|
1667
|
+
val glassesConnected = DeviceStore.get("glasses", "connected") as? Boolean ?: false
|
|
1668
|
+
if (!glassesConnected) throw IllegalStateException("not_connected")
|
|
1669
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1670
|
+
live.sendCameraFovSetting()
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
fun sendCameraFovOverride(
|
|
1674
|
+
requestId: String,
|
|
1675
|
+
leaseId: String,
|
|
1676
|
+
fov: Int,
|
|
1677
|
+
roiPosition: Int,
|
|
1678
|
+
ttlMs: Int,
|
|
1679
|
+
) {
|
|
1680
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1681
|
+
live.sendCameraFovOverride(requestId, leaseId, fov, roiPosition, ttlMs)
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
fun releaseCameraFovOverride(requestId: String, leaseId: String) {
|
|
1685
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1686
|
+
live.releaseCameraFovOverride(requestId, leaseId)
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1652
1689
|
fun sendCameraTuningConfig(requestId: String, anrOn: Boolean, gainOn: Boolean) {
|
|
1653
1690
|
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1654
1691
|
live.sendCameraTuningConfig(requestId, anrOn, gainOn)
|
|
@@ -1657,6 +1694,7 @@ class DeviceManager {
|
|
|
1657
1694
|
fun warmUpCamera(
|
|
1658
1695
|
requestId: String,
|
|
1659
1696
|
size: PhotoSize,
|
|
1697
|
+
mode: PhotoMode = PhotoMode.PHOTO,
|
|
1660
1698
|
exposureTimeNs: Long?,
|
|
1661
1699
|
durationMs: Int,
|
|
1662
1700
|
) {
|
|
@@ -1668,7 +1706,12 @@ class DeviceManager {
|
|
|
1668
1706
|
"unsupported_device",
|
|
1669
1707
|
"This command requires Mentra Live glasses.",
|
|
1670
1708
|
)
|
|
1671
|
-
live.warmUpCamera(requestId, size, exposureTimeNs, durationMs)
|
|
1709
|
+
live.warmUpCamera(requestId, size, mode, exposureTimeNs, durationMs)
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
fun stopCameraWarmUp(requestId: String) {
|
|
1713
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1714
|
+
live.stopCameraWarmUp(requestId)
|
|
1672
1715
|
}
|
|
1673
1716
|
|
|
1674
1717
|
/**
|
|
@@ -128,7 +128,7 @@ object DeviceStore {
|
|
|
128
128
|
store.set("bluetooth", "screen_disabled", false)
|
|
129
129
|
store.set("bluetooth", "button_photo_size", "max")
|
|
130
130
|
store.set("bluetooth", "button_max_recording_time", 10)
|
|
131
|
-
store.set("bluetooth", "camera_fov", mapOf("fov" to
|
|
131
|
+
store.set("bluetooth", "camera_fov", mapOf("fov" to 102, "roi_position" to 0))
|
|
132
132
|
store.set("bluetooth", "button_video_width", 1280)
|
|
133
133
|
store.set("bluetooth", "button_video_height", 720)
|
|
134
134
|
store.set("bluetooth", "button_video_fps", 30)
|