@mentra/bluetooth-sdk 0.1.19 → 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 +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +239 -52
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +65 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +47 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +6 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +381 -97
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- 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 +293 -174
- 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/types/DeviceModels.kt +2 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +6 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +31 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
- package/build/BluetoothSdk.types.d.ts +71 -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 +27 -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 +2 -0
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9 -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 +52 -3
- package/ios/Source/DeviceStore.swift +6 -1
- package/ios/Source/MentraBluetoothSDK.swift +298 -21
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +22 -3
- package/ios/Source/sgcs/G1.swift +2 -2
- package/ios/Source/sgcs/G2.swift +19 -7
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +456 -52
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- 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/ios/Source/utils/G1Text.swift +30 -1
- package/package.json +1 -2
- package/src/BluetoothSdk.types.ts +75 -0
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +68 -13
- package/src/_private/MentraLocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +1 -0
- package/src/_private/photoRequestPayload.ts +3 -3
- package/src/index.ts +10 -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
|
|
@@ -475,7 +479,7 @@ For bare native iOS apps, use the public SwiftPM repository:
|
|
|
475
479
|
https://github.com/Mentra-Community/mentra-bluetooth-sdk-ios.git
|
|
476
480
|
```
|
|
477
481
|
|
|
478
|
-
Select version `0.1.
|
|
482
|
+
Select version `0.1.20`, then add the `MentraBluetoothSDK` product to your app target.
|
|
479
483
|
|
|
480
484
|
For local SDK development, add this package folder directly in Xcode:
|
|
481
485
|
|
|
@@ -1,13 +1,132 @@
|
|
|
1
|
+
@file:Suppress("FunctionName")
|
|
2
|
+
|
|
1
3
|
package com.mentra.bluetoothsdk
|
|
2
4
|
|
|
3
5
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
4
6
|
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
7
|
+
import com.mentra.bluetoothsdk.utils.audio.PcmStreamManager
|
|
8
|
+
import expo.modules.kotlin.exception.CodedException
|
|
5
9
|
import expo.modules.kotlin.functions.Coroutine
|
|
6
10
|
import expo.modules.kotlin.modules.Module
|
|
7
11
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
12
|
+
import expo.modules.kotlin.modules.ModuleDefinitionBuilder
|
|
8
13
|
import kotlinx.coroutines.Dispatchers
|
|
9
14
|
import kotlinx.coroutines.withContext
|
|
10
15
|
|
|
16
|
+
// Expo has no module-level error mapper, so SDK-backed registrations translate
|
|
17
|
+
// core exceptions here while keeping Expo types out of the native SDK API.
|
|
18
|
+
private inline fun <T> withExpoSdkError(block: () -> T): T =
|
|
19
|
+
try {
|
|
20
|
+
block()
|
|
21
|
+
} catch (error: BluetoothSdkException) {
|
|
22
|
+
throw CodedException(error.code, error.message ?: error.code, error)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private suspend inline fun <T> withExpoSdkErrorSuspend(crossinline block: suspend () -> T): T =
|
|
26
|
+
try {
|
|
27
|
+
block()
|
|
28
|
+
} catch (error: BluetoothSdkException) {
|
|
29
|
+
throw CodedException(error.code, error.message ?: error.code, error)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private inline fun <reified R> ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
33
|
+
name: String,
|
|
34
|
+
crossinline body: () -> R,
|
|
35
|
+
) = AsyncFunction<R>(name) { withExpoSdkError { body() } }
|
|
36
|
+
|
|
37
|
+
private inline fun <reified R, reified P0> ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
38
|
+
name: String,
|
|
39
|
+
crossinline body: (P0) -> R,
|
|
40
|
+
) = AsyncFunction<R, P0>(name) { p0 -> withExpoSdkError { body(p0) } }
|
|
41
|
+
|
|
42
|
+
private inline fun <reified R, reified P0, reified P1> ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
43
|
+
name: String,
|
|
44
|
+
crossinline body: (P0, P1) -> R,
|
|
45
|
+
) = AsyncFunction<R, P0, P1>(name) { p0, p1 -> withExpoSdkError { body(p0, p1) } }
|
|
46
|
+
|
|
47
|
+
private inline fun <reified R, reified P0, reified P1, reified P2> ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
48
|
+
name: String,
|
|
49
|
+
crossinline body: (P0, P1, P2) -> R,
|
|
50
|
+
) = AsyncFunction<R, P0, P1, P2>(name) { p0, p1, p2 -> withExpoSdkError { body(p0, p1, p2) } }
|
|
51
|
+
|
|
52
|
+
private inline fun <reified R, reified P0, reified P1, reified P2, reified P3>
|
|
53
|
+
ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
54
|
+
name: String,
|
|
55
|
+
crossinline body: (P0, P1, P2, P3) -> R,
|
|
56
|
+
) = AsyncFunction<R, P0, P1, P2, P3>(name) { p0, p1, p2, p3 ->
|
|
57
|
+
withExpoSdkError { body(p0, p1, p2, p3) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private inline fun <
|
|
61
|
+
reified R,
|
|
62
|
+
reified P0,
|
|
63
|
+
reified P1,
|
|
64
|
+
reified P2,
|
|
65
|
+
reified P3,
|
|
66
|
+
reified P4,
|
|
67
|
+
reified P5,
|
|
68
|
+
reified P6,
|
|
69
|
+
> ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
70
|
+
name: String,
|
|
71
|
+
crossinline body: (P0, P1, P2, P3, P4, P5, P6) -> R,
|
|
72
|
+
) = AsyncFunction<R, P0, P1, P2, P3, P4, P5, P6>(name) { p0, p1, p2, p3, p4, p5, p6 ->
|
|
73
|
+
withExpoSdkError { body(p0, p1, p2, p3, p4, p5, p6) }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private inline fun <reified R> ModuleDefinitionBuilder.SdkFunction(
|
|
77
|
+
name: String,
|
|
78
|
+
crossinline body: () -> R,
|
|
79
|
+
) = Function<R>(name) { withExpoSdkError { body() } }
|
|
80
|
+
|
|
81
|
+
private inline fun <reified R, reified P0> ModuleDefinitionBuilder.SdkFunction(
|
|
82
|
+
name: String,
|
|
83
|
+
crossinline body: (P0) -> R,
|
|
84
|
+
) = Function<R, P0>(name) { p0 -> withExpoSdkError { body(p0) } }
|
|
85
|
+
|
|
86
|
+
private inline fun <reified R> ModuleDefinitionBuilder.SdkCoroutineFunction(
|
|
87
|
+
name: String,
|
|
88
|
+
crossinline body: suspend () -> R,
|
|
89
|
+
) = AsyncFunction(name) Coroutine { -> withExpoSdkErrorSuspend { body() } }
|
|
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
|
+
|
|
11
130
|
class BluetoothSdkModule : Module() {
|
|
12
131
|
private var sdk: MentraBluetoothSdk? = null
|
|
13
132
|
private var deviceManager: DeviceManager? = null
|
|
@@ -183,9 +302,10 @@ class BluetoothSdkModule : Module() {
|
|
|
183
302
|
|
|
184
303
|
private fun requireSdk(): MentraBluetoothSdk =
|
|
185
304
|
sdk
|
|
186
|
-
?: throw
|
|
305
|
+
?: throw CodedException(
|
|
187
306
|
"sdk_not_initialized",
|
|
188
307
|
"Bluetooth SDK is not initialized.",
|
|
308
|
+
null,
|
|
189
309
|
)
|
|
190
310
|
|
|
191
311
|
override fun definition() = ModuleDefinition {
|
|
@@ -280,6 +400,7 @@ class BluetoothSdkModule : Module() {
|
|
|
280
400
|
"module_destroy"
|
|
281
401
|
)
|
|
282
402
|
sdk?.close()
|
|
403
|
+
PcmStreamManager.abortAll()
|
|
283
404
|
sdk = null
|
|
284
405
|
deviceManager = null
|
|
285
406
|
}
|
|
@@ -335,11 +456,11 @@ class BluetoothSdkModule : Module() {
|
|
|
335
456
|
|
|
336
457
|
// MARK: - Display Commands
|
|
337
458
|
|
|
338
|
-
|
|
459
|
+
SdkAsyncFunction("displayEvent") { params: Map<String, Any> ->
|
|
339
460
|
sdk?.displayEvent(DisplayEventRequest(params))
|
|
340
461
|
}
|
|
341
462
|
|
|
342
|
-
|
|
463
|
+
SdkAsyncFunction("displayText") { text: String, x: Int?, y: Int?, size: Int? ->
|
|
343
464
|
sdk?.displayText(
|
|
344
465
|
text = text,
|
|
345
466
|
x = x ?: 0,
|
|
@@ -348,34 +469,35 @@ class BluetoothSdkModule : Module() {
|
|
|
348
469
|
)
|
|
349
470
|
}
|
|
350
471
|
|
|
351
|
-
|
|
472
|
+
SdkAsyncFunction("clearDisplay") { -> sdk?.clearDisplay() }
|
|
352
473
|
|
|
353
474
|
// MARK: - Connection Commands
|
|
354
475
|
|
|
355
|
-
|
|
476
|
+
SdkAsyncFunction("connectDefault") { -> sdk?.connectDefault() }
|
|
356
477
|
|
|
357
|
-
|
|
478
|
+
SdkAsyncFunction("connectDefaultWithOptions") { options: Map<String, Any> ->
|
|
358
479
|
sdk?.connectDefault(options.toMentraConnectOptions())
|
|
359
480
|
}
|
|
360
481
|
|
|
361
|
-
|
|
482
|
+
SdkAsyncFunction("setDefaultDevice") { device: Map<String, Any>? ->
|
|
362
483
|
sdk?.setDefaultDevice(device.toMentraDevice())
|
|
363
484
|
}
|
|
364
485
|
|
|
365
|
-
|
|
486
|
+
SdkAsyncFunction("clearDefaultDevice") { -> sdk?.clearDefaultDevice() }
|
|
366
487
|
|
|
367
|
-
|
|
488
|
+
SdkAsyncFunction("connectWithOptions") { device: Map<String, Any>, options: Map<String, Any> ->
|
|
368
489
|
sdk?.connect(
|
|
369
|
-
device.toMentraDevice()
|
|
490
|
+
device.toMentraDevice()
|
|
491
|
+
?: throw IllegalArgumentException("connect requires a Device with model and name."),
|
|
370
492
|
options.toMentraConnectOptions(),
|
|
371
493
|
)
|
|
372
494
|
}
|
|
373
495
|
|
|
374
|
-
|
|
496
|
+
SdkAsyncFunction("connectSimulated") { -> sdk?.connectSimulated() }
|
|
375
497
|
|
|
376
|
-
|
|
498
|
+
SdkAsyncFunction("disconnect") { -> sdk?.disconnect() }
|
|
377
499
|
|
|
378
|
-
|
|
500
|
+
SdkAsyncFunction("forget") { -> sdk?.forget() }
|
|
379
501
|
|
|
380
502
|
AsyncFunction("connectDefaultController") { deviceManager?.connectDefaultController() }
|
|
381
503
|
|
|
@@ -383,15 +505,15 @@ class BluetoothSdkModule : Module() {
|
|
|
383
505
|
|
|
384
506
|
AsyncFunction("forgetController") { deviceManager?.forgetController() }
|
|
385
507
|
|
|
386
|
-
|
|
508
|
+
SdkAsyncFunction("startScan") { model: String ->
|
|
387
509
|
sdk?.startScan(DeviceModel.fromDeviceType(model))
|
|
388
510
|
}
|
|
389
511
|
|
|
390
|
-
|
|
512
|
+
SdkAsyncFunction("stopScan") { -> sdk?.stopScan() }
|
|
391
513
|
|
|
392
|
-
|
|
514
|
+
SdkAsyncFunction("cancelConnectionAttempt") { -> sdk?.cancelConnectionAttempt() }
|
|
393
515
|
|
|
394
|
-
|
|
516
|
+
SdkAsyncFunction("showDashboard") { -> sdk?.showDashboard() }
|
|
395
517
|
|
|
396
518
|
AsyncFunction("ping") { deviceManager?.ping() }
|
|
397
519
|
|
|
@@ -409,51 +531,54 @@ class BluetoothSdkModule : Module() {
|
|
|
409
531
|
|
|
410
532
|
// MARK: - Incident Reporting
|
|
411
533
|
|
|
412
|
-
|
|
534
|
+
SdkAsyncFunction("sendIncidentId") { incidentId: String, apiBaseUrl: String? ->
|
|
413
535
|
sdk?.sendIncidentId(incidentId, apiBaseUrl)
|
|
414
536
|
}
|
|
415
537
|
|
|
416
538
|
// MARK: - WiFi Commands
|
|
417
539
|
|
|
418
|
-
|
|
540
|
+
SdkCoroutineFunction("requestWifiScan") { -> requireSdk().requestWifiScan().map { it.toMap() } }
|
|
419
541
|
|
|
420
|
-
|
|
542
|
+
SdkCoroutineFunction("sendWifiCredentials") { ssid: String, password: String ->
|
|
421
543
|
requireSdk().sendWifiCredentials(ssid, password).values
|
|
422
544
|
}
|
|
423
545
|
|
|
424
|
-
|
|
546
|
+
SdkCoroutineFunction("forgetWifiNetwork") { ssid: String ->
|
|
547
|
+
requireSdk().forgetWifiNetwork(ssid).values
|
|
548
|
+
}
|
|
425
549
|
|
|
426
|
-
|
|
550
|
+
SdkCoroutineFunction("setHotspotState") { enabled: Boolean ->
|
|
427
551
|
requireSdk().setHotspotState(enabled).values
|
|
428
552
|
}
|
|
429
553
|
|
|
430
|
-
|
|
554
|
+
SdkAsyncFunction("setSystemTime") { timestampMs: Double ->
|
|
431
555
|
sdk?.setSystemTime(timestampMs.toLong())
|
|
432
556
|
}
|
|
433
557
|
|
|
434
558
|
// MARK: - Gallery Commands
|
|
435
559
|
|
|
436
|
-
|
|
560
|
+
SdkCoroutineFunction("setGalleryModeEnabled") { enabled: Boolean ->
|
|
437
561
|
requireSdk().setGalleryModeEnabled(enabled).values
|
|
438
562
|
}
|
|
439
563
|
|
|
440
|
-
|
|
564
|
+
SdkAsyncFunction("setVoiceActivityDetectionEnabled") { enabled: Boolean ->
|
|
441
565
|
sdk?.setVoiceActivityDetectionEnabled(enabled)
|
|
442
566
|
}
|
|
443
567
|
|
|
444
|
-
|
|
568
|
+
@Suppress("DEPRECATION")
|
|
569
|
+
SdkCoroutineFunction("setPhotoCaptureDefaults") { params: Map<String, Any?> ->
|
|
445
570
|
requireSdk().setPhotoCaptureDefaults(params.toPhotoCaptureDefaults()).values
|
|
446
571
|
}
|
|
447
572
|
|
|
448
|
-
|
|
573
|
+
SdkCoroutineFunction("setVideoRecordingDefaults") { width: Int, height: Int, fps: Int ->
|
|
449
574
|
requireSdk().setVideoRecordingDefaults(VideoRecordingDefaults(width, height, fps)).values
|
|
450
575
|
}
|
|
451
576
|
|
|
452
|
-
|
|
577
|
+
SdkCoroutineFunction("setMaxVideoRecordingDuration") { minutes: Int ->
|
|
453
578
|
requireSdk().setMaxVideoRecordingDuration(minutes).values
|
|
454
579
|
}
|
|
455
580
|
|
|
456
|
-
|
|
581
|
+
SdkCoroutineFunction("setCameraFov") { fov: Map<String, Any> ->
|
|
457
582
|
val value = (fov["fov"] as? Number)?.toInt() ?: CameraFov.DEFAULT_FOV
|
|
458
583
|
val roiPosition = CameraRoiPosition.fromValue(
|
|
459
584
|
(fov["roiPosition"] as? Number)?.toInt()
|
|
@@ -462,13 +587,41 @@ class BluetoothSdkModule : Module() {
|
|
|
462
587
|
requireSdk().setCameraFov(CameraFov(value, roiPosition)).values
|
|
463
588
|
}
|
|
464
589
|
|
|
465
|
-
|
|
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 ->
|
|
466
619
|
requireSdk().setCameraTuningConfig(anrOn, gainOn).values
|
|
467
620
|
}
|
|
468
621
|
|
|
469
|
-
|
|
622
|
+
SdkCoroutineFunction("queryGalleryStatus") { -> requireSdk().queryGalleryStatus().values }
|
|
470
623
|
|
|
471
|
-
|
|
624
|
+
SdkCoroutineFunction("requestPhoto") { params: Map<String, Any?> ->
|
|
472
625
|
// JS may pass null for optional fields; Map<String, Any> rejects null values at the bridge.
|
|
473
626
|
val sanitized =
|
|
474
627
|
params.mapNotNull { (key, value) ->
|
|
@@ -476,37 +629,42 @@ class BluetoothSdkModule : Module() {
|
|
|
476
629
|
}.toMap()
|
|
477
630
|
val req = PhotoRequest.fromMap(sanitized)
|
|
478
631
|
Bridge.log(
|
|
479
|
-
"NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} size=${req.size} compress=${req.compress} sound=${req.sound} exposureTimeNs=${req.exposureTimeNs} iso=${req.iso}"
|
|
632
|
+
"NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} size=${req.size} mode=${req.mode.value} compress=${req.compress} sound=${req.sound} exposureTimeNs=${req.exposureTimeNs} iso=${req.iso}"
|
|
480
633
|
)
|
|
481
634
|
requireSdk().requestPhoto(req).values
|
|
482
635
|
}
|
|
483
636
|
|
|
484
|
-
|
|
637
|
+
SdkCoroutineFunction("warmUpCamera") { params: Map<String, Any?> ->
|
|
485
638
|
val requestId = params["requestId"] as? String
|
|
486
639
|
val size = PhotoSize.fromValue(params["size"] as? String)
|
|
640
|
+
val mode = PhotoMode.fromValue(params["mode"] as? String)
|
|
487
641
|
val exposureRaw = (params["exposureTimeNs"] as? Number)?.toDouble()
|
|
488
642
|
val exposureTimeNs =
|
|
489
643
|
if (exposureRaw != null && exposureRaw.isFinite() && exposureRaw > 0) exposureRaw.toLong() else null
|
|
490
644
|
val durationRaw = (params["durationMs"] as? Number)?.toInt() ?: 0
|
|
491
645
|
val durationMs = if (durationRaw > 0) durationRaw else 15000
|
|
492
|
-
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)
|
|
493
651
|
}
|
|
494
652
|
|
|
495
653
|
// MARK: - OTA Commands
|
|
496
654
|
|
|
497
|
-
|
|
655
|
+
SdkFunction("setOtaVersionUrl") { otaVersionUrl: String ->
|
|
498
656
|
requireSdk().setOtaVersionUrl(otaVersionUrl)
|
|
499
657
|
}
|
|
500
658
|
|
|
501
|
-
|
|
659
|
+
SdkFunction("getOtaVersionUrl") { -> requireSdk().getOtaVersionUrl() }
|
|
502
660
|
|
|
503
661
|
// Runs on Dispatchers.IO, not the shared Expo AsyncFunctionQueue:
|
|
504
662
|
// manifest fetches and version waits can block for several seconds.
|
|
505
|
-
|
|
663
|
+
SdkCoroutineFunction("checkForOtaUpdate") { ->
|
|
506
664
|
withContext(Dispatchers.IO) { requireSdk().checkForOtaUpdate() }
|
|
507
665
|
}
|
|
508
666
|
|
|
509
|
-
|
|
667
|
+
SdkCoroutineFunction("startOtaUpdate") { otaVersionUrl: String? ->
|
|
510
668
|
val sdk = requireSdk()
|
|
511
669
|
if (otaVersionUrl.isNullOrBlank()) {
|
|
512
670
|
sdk.startOtaUpdate().values
|
|
@@ -515,21 +673,21 @@ class BluetoothSdkModule : Module() {
|
|
|
515
673
|
}
|
|
516
674
|
}
|
|
517
675
|
|
|
518
|
-
|
|
676
|
+
SdkCoroutineFunction("sendOtaQueryStatus") { -> requireSdk().sendOtaQueryStatus().values }
|
|
519
677
|
|
|
520
678
|
// MARK: - Version Info Commands
|
|
521
679
|
|
|
522
|
-
|
|
680
|
+
SdkCoroutineFunction("requestVersionInfo") { -> requireSdk().requestVersionInfo().toMap() }
|
|
523
681
|
|
|
524
682
|
// MARK: - Power Control Commands
|
|
525
683
|
|
|
526
|
-
|
|
684
|
+
SdkAsyncFunction("sendShutdown") { -> sdk?.sendShutdown() }
|
|
527
685
|
|
|
528
|
-
|
|
686
|
+
SdkAsyncFunction("sendReboot") { -> sdk?.sendReboot() }
|
|
529
687
|
|
|
530
688
|
// MARK: - Video Recording Commands
|
|
531
689
|
|
|
532
|
-
|
|
690
|
+
SdkCoroutineFunction("startVideoRecording") {
|
|
533
691
|
requestId: String,
|
|
534
692
|
save: Boolean,
|
|
535
693
|
sound: Boolean,
|
|
@@ -553,7 +711,7 @@ class BluetoothSdkModule : Module() {
|
|
|
553
711
|
|
|
554
712
|
// webhookUrl/authToken are supplied at stop (not start) so the token is
|
|
555
713
|
// fresh when the upload runs. Empty/null webhook = keep on device.
|
|
556
|
-
|
|
714
|
+
SdkCoroutineFunction("stopVideoRecording") {
|
|
557
715
|
requestId: String,
|
|
558
716
|
webhookUrl: String?,
|
|
559
717
|
authToken: String? ->
|
|
@@ -562,23 +720,23 @@ class BluetoothSdkModule : Module() {
|
|
|
562
720
|
|
|
563
721
|
// MARK: - Stream Commands
|
|
564
722
|
|
|
565
|
-
|
|
723
|
+
SdkCoroutineFunction("startStream") { params: Map<String, Any> ->
|
|
566
724
|
requireSdk().startStream(StreamRequest.fromMap(params)).values
|
|
567
725
|
}
|
|
568
726
|
|
|
569
|
-
|
|
727
|
+
SdkCoroutineFunction("startExternallyManagedStream") { params: Map<String, Any> ->
|
|
570
728
|
requireSdk().startExternallyManagedStream(StreamRequest.fromMap(params)).values
|
|
571
729
|
}
|
|
572
730
|
|
|
573
|
-
|
|
731
|
+
SdkCoroutineFunction("stopStream") { -> requireSdk().stopStream().values }
|
|
574
732
|
|
|
575
|
-
|
|
733
|
+
SdkAsyncFunction("sendExternallyManagedStreamKeepAlive") { params: Map<String, Any> ->
|
|
576
734
|
sdk?.sendExternallyManagedStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
|
|
577
735
|
}
|
|
578
736
|
|
|
579
737
|
// MARK: - Microphone Commands
|
|
580
738
|
|
|
581
|
-
|
|
739
|
+
SdkAsyncFunction("setMicState") {
|
|
582
740
|
enabled: Boolean,
|
|
583
741
|
useGlassesMic: Boolean?,
|
|
584
742
|
sendTranscript: Boolean?,
|
|
@@ -600,10 +758,39 @@ class BluetoothSdkModule : Module() {
|
|
|
600
758
|
|
|
601
759
|
// MARK: - Audio Playback Monitoring
|
|
602
760
|
|
|
603
|
-
|
|
761
|
+
SdkAsyncFunction("setOwnAppAudioPlaying") { playing: Boolean ->
|
|
604
762
|
sdk?.setOwnAppAudioPlaying(playing)
|
|
605
763
|
}
|
|
606
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
|
+
|
|
607
794
|
// *Blocking on Dispatchers.IO, not the shared AsyncFunctionQueue: these wait on
|
|
608
795
|
// a CountDownLatch (up to 5s) for a BLE round-trip, which would otherwise stall
|
|
609
796
|
// every other native call queued behind them.
|
|
@@ -619,7 +806,7 @@ class BluetoothSdkModule : Module() {
|
|
|
619
806
|
|
|
620
807
|
// MARK: - RGB LED Control
|
|
621
808
|
|
|
622
|
-
|
|
809
|
+
SdkCoroutineFunction("rgbLedControl") {
|
|
623
810
|
requestId: String,
|
|
624
811
|
packageName: String?,
|
|
625
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
|
|