@mentra/bluetooth-sdk 0.1.18 → 0.1.20
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 +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +135 -51
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +120 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +551 -159
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +361 -67
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +7501 -3704
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +134 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleWireProtocol.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +4 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900LengthCodec.java +115 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +109 -73
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +93 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +111 -11
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -9
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +17 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +158 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
- package/build/BluetoothSdk.types.d.ts +37 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.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 +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -0
- package/build/index.js.map +1 -1
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +2 -0
- package/build/types/index.js.map +1 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +136 -30
- package/ios/Source/DeviceStore.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +40 -24
- package/ios/Source/OtaManifest.swift +1 -1
- package/ios/Source/camera/CameraModels.swift +17 -3
- package/ios/Source/sgcs/G1.swift +1 -1
- package/ios/Source/sgcs/G2.swift +518 -11
- package/ios/Source/sgcs/MentraLive.swift +443 -42
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- package/ios/Source/sgcs/MentraNex.swift +416 -32
- package/ios/Source/sgcs/SGCManager.swift +155 -0
- package/ios/Source/sgcs/mentraos_ble.pb.swift +726 -122
- package/ios/Source/status/DeviceStatus.swift +0 -8
- package/ios/Source/utils/BleJsonCompact.swift +395 -0
- package/ios/Source/utils/BleWireProtocol.swift +92 -0
- package/ios/Source/utils/G1Text.swift +30 -1
- package/ios/Source/utils/MessageChunkReassembler.swift +82 -0
- package/ios/Source/utils/MessageChunker.swift +73 -0
- package/package.json +14 -7
- package/src/BluetoothSdk.types.ts +39 -1
- package/src/_private/BluetoothSdkModule.ts +4 -0
- package/src/_private/photoRequestPayload.ts +1 -0
- package/src/index.ts +6 -0
- package/src/types/index.ts +7 -0
package/README.md
CHANGED
|
@@ -475,7 +475,7 @@ For bare native iOS apps, use the public SwiftPM repository:
|
|
|
475
475
|
https://github.com/Mentra-Community/mentra-bluetooth-sdk-ios.git
|
|
476
476
|
```
|
|
477
477
|
|
|
478
|
-
Select version `0.1.
|
|
478
|
+
Select version `0.1.20`, then add the `MentraBluetoothSDK` product to your app target.
|
|
479
479
|
|
|
480
480
|
For local SDK development, add this package folder directly in Xcode:
|
|
481
481
|
|
|
@@ -1,13 +1,92 @@
|
|
|
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 expo.modules.kotlin.exception.CodedException
|
|
5
8
|
import expo.modules.kotlin.functions.Coroutine
|
|
6
9
|
import expo.modules.kotlin.modules.Module
|
|
7
10
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
11
|
+
import expo.modules.kotlin.modules.ModuleDefinitionBuilder
|
|
8
12
|
import kotlinx.coroutines.Dispatchers
|
|
9
13
|
import kotlinx.coroutines.withContext
|
|
10
14
|
|
|
15
|
+
// Expo has no module-level error mapper, so SDK-backed registrations translate
|
|
16
|
+
// core exceptions here while keeping Expo types out of the native SDK API.
|
|
17
|
+
private inline fun <T> withExpoSdkError(block: () -> T): T =
|
|
18
|
+
try {
|
|
19
|
+
block()
|
|
20
|
+
} catch (error: BluetoothSdkException) {
|
|
21
|
+
throw CodedException(error.code, error.message ?: error.code, error)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private suspend inline fun <T> withExpoSdkErrorSuspend(crossinline block: suspend () -> T): T =
|
|
25
|
+
try {
|
|
26
|
+
block()
|
|
27
|
+
} catch (error: BluetoothSdkException) {
|
|
28
|
+
throw CodedException(error.code, error.message ?: error.code, error)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private inline fun <reified R> ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
32
|
+
name: String,
|
|
33
|
+
crossinline body: () -> R,
|
|
34
|
+
) = AsyncFunction<R>(name) { withExpoSdkError { body() } }
|
|
35
|
+
|
|
36
|
+
private inline fun <reified R, reified P0> ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
37
|
+
name: String,
|
|
38
|
+
crossinline body: (P0) -> R,
|
|
39
|
+
) = AsyncFunction<R, P0>(name) { p0 -> withExpoSdkError { body(p0) } }
|
|
40
|
+
|
|
41
|
+
private inline fun <reified R, reified P0, reified P1> ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
42
|
+
name: String,
|
|
43
|
+
crossinline body: (P0, P1) -> R,
|
|
44
|
+
) = AsyncFunction<R, P0, P1>(name) { p0, p1 -> withExpoSdkError { body(p0, p1) } }
|
|
45
|
+
|
|
46
|
+
private inline fun <reified R, reified P0, reified P1, reified P2> ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
47
|
+
name: String,
|
|
48
|
+
crossinline body: (P0, P1, P2) -> R,
|
|
49
|
+
) = AsyncFunction<R, P0, P1, P2>(name) { p0, p1, p2 -> withExpoSdkError { body(p0, p1, p2) } }
|
|
50
|
+
|
|
51
|
+
private inline fun <reified R, reified P0, reified P1, reified P2, reified P3>
|
|
52
|
+
ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
53
|
+
name: String,
|
|
54
|
+
crossinline body: (P0, P1, P2, P3) -> R,
|
|
55
|
+
) = AsyncFunction<R, P0, P1, P2, P3>(name) { p0, p1, p2, p3 ->
|
|
56
|
+
withExpoSdkError { body(p0, p1, p2, p3) }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private inline fun <
|
|
60
|
+
reified R,
|
|
61
|
+
reified P0,
|
|
62
|
+
reified P1,
|
|
63
|
+
reified P2,
|
|
64
|
+
reified P3,
|
|
65
|
+
reified P4,
|
|
66
|
+
reified P5,
|
|
67
|
+
reified P6,
|
|
68
|
+
> ModuleDefinitionBuilder.SdkAsyncFunction(
|
|
69
|
+
name: String,
|
|
70
|
+
crossinline body: (P0, P1, P2, P3, P4, P5, P6) -> R,
|
|
71
|
+
) = AsyncFunction<R, P0, P1, P2, P3, P4, P5, P6>(name) { p0, p1, p2, p3, p4, p5, p6 ->
|
|
72
|
+
withExpoSdkError { body(p0, p1, p2, p3, p4, p5, p6) }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private inline fun <reified R> ModuleDefinitionBuilder.SdkFunction(
|
|
76
|
+
name: String,
|
|
77
|
+
crossinline body: () -> R,
|
|
78
|
+
) = Function<R>(name) { withExpoSdkError { body() } }
|
|
79
|
+
|
|
80
|
+
private inline fun <reified R, reified P0> ModuleDefinitionBuilder.SdkFunction(
|
|
81
|
+
name: String,
|
|
82
|
+
crossinline body: (P0) -> R,
|
|
83
|
+
) = Function<R, P0>(name) { p0 -> withExpoSdkError { body(p0) } }
|
|
84
|
+
|
|
85
|
+
private inline fun <reified R> ModuleDefinitionBuilder.SdkCoroutineFunction(
|
|
86
|
+
name: String,
|
|
87
|
+
crossinline body: suspend () -> R,
|
|
88
|
+
) = AsyncFunction(name) Coroutine { -> withExpoSdkErrorSuspend { body() } }
|
|
89
|
+
|
|
11
90
|
class BluetoothSdkModule : Module() {
|
|
12
91
|
private var sdk: MentraBluetoothSdk? = null
|
|
13
92
|
private var deviceManager: DeviceManager? = null
|
|
@@ -183,9 +262,10 @@ class BluetoothSdkModule : Module() {
|
|
|
183
262
|
|
|
184
263
|
private fun requireSdk(): MentraBluetoothSdk =
|
|
185
264
|
sdk
|
|
186
|
-
?: throw
|
|
265
|
+
?: throw CodedException(
|
|
187
266
|
"sdk_not_initialized",
|
|
188
267
|
"Bluetooth SDK is not initialized.",
|
|
268
|
+
null,
|
|
189
269
|
)
|
|
190
270
|
|
|
191
271
|
override fun definition() = ModuleDefinition {
|
|
@@ -335,11 +415,11 @@ class BluetoothSdkModule : Module() {
|
|
|
335
415
|
|
|
336
416
|
// MARK: - Display Commands
|
|
337
417
|
|
|
338
|
-
|
|
418
|
+
SdkAsyncFunction("displayEvent") { params: Map<String, Any> ->
|
|
339
419
|
sdk?.displayEvent(DisplayEventRequest(params))
|
|
340
420
|
}
|
|
341
421
|
|
|
342
|
-
|
|
422
|
+
SdkAsyncFunction("displayText") { text: String, x: Int?, y: Int?, size: Int? ->
|
|
343
423
|
sdk?.displayText(
|
|
344
424
|
text = text,
|
|
345
425
|
x = x ?: 0,
|
|
@@ -348,34 +428,35 @@ class BluetoothSdkModule : Module() {
|
|
|
348
428
|
)
|
|
349
429
|
}
|
|
350
430
|
|
|
351
|
-
|
|
431
|
+
SdkAsyncFunction("clearDisplay") { -> sdk?.clearDisplay() }
|
|
352
432
|
|
|
353
433
|
// MARK: - Connection Commands
|
|
354
434
|
|
|
355
|
-
|
|
435
|
+
SdkAsyncFunction("connectDefault") { -> sdk?.connectDefault() }
|
|
356
436
|
|
|
357
|
-
|
|
437
|
+
SdkAsyncFunction("connectDefaultWithOptions") { options: Map<String, Any> ->
|
|
358
438
|
sdk?.connectDefault(options.toMentraConnectOptions())
|
|
359
439
|
}
|
|
360
440
|
|
|
361
|
-
|
|
441
|
+
SdkAsyncFunction("setDefaultDevice") { device: Map<String, Any>? ->
|
|
362
442
|
sdk?.setDefaultDevice(device.toMentraDevice())
|
|
363
443
|
}
|
|
364
444
|
|
|
365
|
-
|
|
445
|
+
SdkAsyncFunction("clearDefaultDevice") { -> sdk?.clearDefaultDevice() }
|
|
366
446
|
|
|
367
|
-
|
|
447
|
+
SdkAsyncFunction("connectWithOptions") { device: Map<String, Any>, options: Map<String, Any> ->
|
|
368
448
|
sdk?.connect(
|
|
369
|
-
device.toMentraDevice()
|
|
449
|
+
device.toMentraDevice()
|
|
450
|
+
?: throw IllegalArgumentException("connect requires a Device with model and name."),
|
|
370
451
|
options.toMentraConnectOptions(),
|
|
371
452
|
)
|
|
372
453
|
}
|
|
373
454
|
|
|
374
|
-
|
|
455
|
+
SdkAsyncFunction("connectSimulated") { -> sdk?.connectSimulated() }
|
|
375
456
|
|
|
376
|
-
|
|
457
|
+
SdkAsyncFunction("disconnect") { -> sdk?.disconnect() }
|
|
377
458
|
|
|
378
|
-
|
|
459
|
+
SdkAsyncFunction("forget") { -> sdk?.forget() }
|
|
379
460
|
|
|
380
461
|
AsyncFunction("connectDefaultController") { deviceManager?.connectDefaultController() }
|
|
381
462
|
|
|
@@ -383,15 +464,15 @@ class BluetoothSdkModule : Module() {
|
|
|
383
464
|
|
|
384
465
|
AsyncFunction("forgetController") { deviceManager?.forgetController() }
|
|
385
466
|
|
|
386
|
-
|
|
467
|
+
SdkAsyncFunction("startScan") { model: String ->
|
|
387
468
|
sdk?.startScan(DeviceModel.fromDeviceType(model))
|
|
388
469
|
}
|
|
389
470
|
|
|
390
|
-
|
|
471
|
+
SdkAsyncFunction("stopScan") { -> sdk?.stopScan() }
|
|
391
472
|
|
|
392
|
-
|
|
473
|
+
SdkAsyncFunction("cancelConnectionAttempt") { -> sdk?.cancelConnectionAttempt() }
|
|
393
474
|
|
|
394
|
-
|
|
475
|
+
SdkAsyncFunction("showDashboard") { -> sdk?.showDashboard() }
|
|
395
476
|
|
|
396
477
|
AsyncFunction("ping") { deviceManager?.ping() }
|
|
397
478
|
|
|
@@ -409,51 +490,54 @@ class BluetoothSdkModule : Module() {
|
|
|
409
490
|
|
|
410
491
|
// MARK: - Incident Reporting
|
|
411
492
|
|
|
412
|
-
|
|
493
|
+
SdkAsyncFunction("sendIncidentId") { incidentId: String, apiBaseUrl: String? ->
|
|
413
494
|
sdk?.sendIncidentId(incidentId, apiBaseUrl)
|
|
414
495
|
}
|
|
415
496
|
|
|
416
497
|
// MARK: - WiFi Commands
|
|
417
498
|
|
|
418
|
-
|
|
499
|
+
SdkAsyncFunction("requestWifiScan") { -> requireSdk().requestWifiScan().map { it.toMap() } }
|
|
419
500
|
|
|
420
|
-
|
|
501
|
+
SdkAsyncFunction("sendWifiCredentials") { ssid: String, password: String ->
|
|
421
502
|
requireSdk().sendWifiCredentials(ssid, password).values
|
|
422
503
|
}
|
|
423
504
|
|
|
424
|
-
|
|
505
|
+
SdkAsyncFunction("forgetWifiNetwork") { ssid: String ->
|
|
506
|
+
requireSdk().forgetWifiNetwork(ssid).values
|
|
507
|
+
}
|
|
425
508
|
|
|
426
|
-
|
|
509
|
+
SdkAsyncFunction("setHotspotState") { enabled: Boolean ->
|
|
427
510
|
requireSdk().setHotspotState(enabled).values
|
|
428
511
|
}
|
|
429
512
|
|
|
430
|
-
|
|
513
|
+
SdkAsyncFunction("setSystemTime") { timestampMs: Double ->
|
|
431
514
|
sdk?.setSystemTime(timestampMs.toLong())
|
|
432
515
|
}
|
|
433
516
|
|
|
434
517
|
// MARK: - Gallery Commands
|
|
435
518
|
|
|
436
|
-
|
|
519
|
+
SdkAsyncFunction("setGalleryModeEnabled") { enabled: Boolean ->
|
|
437
520
|
requireSdk().setGalleryModeEnabled(enabled).values
|
|
438
521
|
}
|
|
439
522
|
|
|
440
|
-
|
|
523
|
+
SdkAsyncFunction("setVoiceActivityDetectionEnabled") { enabled: Boolean ->
|
|
441
524
|
sdk?.setVoiceActivityDetectionEnabled(enabled)
|
|
442
525
|
}
|
|
443
526
|
|
|
444
|
-
|
|
527
|
+
@Suppress("DEPRECATION")
|
|
528
|
+
SdkAsyncFunction("setPhotoCaptureDefaults") { params: Map<String, Any?> ->
|
|
445
529
|
requireSdk().setPhotoCaptureDefaults(params.toPhotoCaptureDefaults()).values
|
|
446
530
|
}
|
|
447
531
|
|
|
448
|
-
|
|
532
|
+
SdkAsyncFunction("setVideoRecordingDefaults") { width: Int, height: Int, fps: Int ->
|
|
449
533
|
requireSdk().setVideoRecordingDefaults(VideoRecordingDefaults(width, height, fps)).values
|
|
450
534
|
}
|
|
451
535
|
|
|
452
|
-
|
|
536
|
+
SdkAsyncFunction("setMaxVideoRecordingDuration") { minutes: Int ->
|
|
453
537
|
requireSdk().setMaxVideoRecordingDuration(minutes).values
|
|
454
538
|
}
|
|
455
539
|
|
|
456
|
-
|
|
540
|
+
SdkAsyncFunction("setCameraFov") { fov: Map<String, Any> ->
|
|
457
541
|
val value = (fov["fov"] as? Number)?.toInt() ?: CameraFov.DEFAULT_FOV
|
|
458
542
|
val roiPosition = CameraRoiPosition.fromValue(
|
|
459
543
|
(fov["roiPosition"] as? Number)?.toInt()
|
|
@@ -462,13 +546,13 @@ class BluetoothSdkModule : Module() {
|
|
|
462
546
|
requireSdk().setCameraFov(CameraFov(value, roiPosition)).values
|
|
463
547
|
}
|
|
464
548
|
|
|
465
|
-
|
|
549
|
+
SdkAsyncFunction("setCameraTuningConfig") { anrOn: Boolean, gainOn: Boolean ->
|
|
466
550
|
requireSdk().setCameraTuningConfig(anrOn, gainOn).values
|
|
467
551
|
}
|
|
468
552
|
|
|
469
|
-
|
|
553
|
+
SdkAsyncFunction("queryGalleryStatus") { -> requireSdk().queryGalleryStatus().values }
|
|
470
554
|
|
|
471
|
-
|
|
555
|
+
SdkAsyncFunction("requestPhoto") { params: Map<String, Any?> ->
|
|
472
556
|
// JS may pass null for optional fields; Map<String, Any> rejects null values at the bridge.
|
|
473
557
|
val sanitized =
|
|
474
558
|
params.mapNotNull { (key, value) ->
|
|
@@ -476,12 +560,12 @@ class BluetoothSdkModule : Module() {
|
|
|
476
560
|
}.toMap()
|
|
477
561
|
val req = PhotoRequest.fromMap(sanitized)
|
|
478
562
|
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}"
|
|
563
|
+
"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
564
|
)
|
|
481
565
|
requireSdk().requestPhoto(req).values
|
|
482
566
|
}
|
|
483
567
|
|
|
484
|
-
|
|
568
|
+
SdkAsyncFunction("warmUpCamera") { params: Map<String, Any?> ->
|
|
485
569
|
val requestId = params["requestId"] as? String
|
|
486
570
|
val size = PhotoSize.fromValue(params["size"] as? String)
|
|
487
571
|
val exposureRaw = (params["exposureTimeNs"] as? Number)?.toDouble()
|
|
@@ -494,19 +578,19 @@ class BluetoothSdkModule : Module() {
|
|
|
494
578
|
|
|
495
579
|
// MARK: - OTA Commands
|
|
496
580
|
|
|
497
|
-
|
|
581
|
+
SdkFunction("setOtaVersionUrl") { otaVersionUrl: String ->
|
|
498
582
|
requireSdk().setOtaVersionUrl(otaVersionUrl)
|
|
499
583
|
}
|
|
500
584
|
|
|
501
|
-
|
|
585
|
+
SdkFunction("getOtaVersionUrl") { -> requireSdk().getOtaVersionUrl() }
|
|
502
586
|
|
|
503
587
|
// Runs on Dispatchers.IO, not the shared Expo AsyncFunctionQueue:
|
|
504
588
|
// manifest fetches and version waits can block for several seconds.
|
|
505
|
-
|
|
589
|
+
SdkCoroutineFunction("checkForOtaUpdate") {
|
|
506
590
|
withContext(Dispatchers.IO) { requireSdk().checkForOtaUpdate() }
|
|
507
591
|
}
|
|
508
592
|
|
|
509
|
-
|
|
593
|
+
SdkAsyncFunction("startOtaUpdate") { otaVersionUrl: String? ->
|
|
510
594
|
val sdk = requireSdk()
|
|
511
595
|
if (otaVersionUrl.isNullOrBlank()) {
|
|
512
596
|
sdk.startOtaUpdate().values
|
|
@@ -515,21 +599,21 @@ class BluetoothSdkModule : Module() {
|
|
|
515
599
|
}
|
|
516
600
|
}
|
|
517
601
|
|
|
518
|
-
|
|
602
|
+
SdkAsyncFunction("sendOtaQueryStatus") { -> requireSdk().sendOtaQueryStatus().values }
|
|
519
603
|
|
|
520
604
|
// MARK: - Version Info Commands
|
|
521
605
|
|
|
522
|
-
|
|
606
|
+
SdkAsyncFunction("requestVersionInfo") { -> requireSdk().requestVersionInfo().toMap() }
|
|
523
607
|
|
|
524
608
|
// MARK: - Power Control Commands
|
|
525
609
|
|
|
526
|
-
|
|
610
|
+
SdkAsyncFunction("sendShutdown") { -> sdk?.sendShutdown() }
|
|
527
611
|
|
|
528
|
-
|
|
612
|
+
SdkAsyncFunction("sendReboot") { -> sdk?.sendReboot() }
|
|
529
613
|
|
|
530
614
|
// MARK: - Video Recording Commands
|
|
531
615
|
|
|
532
|
-
|
|
616
|
+
SdkAsyncFunction("startVideoRecording") {
|
|
533
617
|
requestId: String,
|
|
534
618
|
save: Boolean,
|
|
535
619
|
sound: Boolean,
|
|
@@ -553,7 +637,7 @@ class BluetoothSdkModule : Module() {
|
|
|
553
637
|
|
|
554
638
|
// webhookUrl/authToken are supplied at stop (not start) so the token is
|
|
555
639
|
// fresh when the upload runs. Empty/null webhook = keep on device.
|
|
556
|
-
|
|
640
|
+
SdkAsyncFunction("stopVideoRecording") {
|
|
557
641
|
requestId: String,
|
|
558
642
|
webhookUrl: String?,
|
|
559
643
|
authToken: String? ->
|
|
@@ -562,23 +646,23 @@ class BluetoothSdkModule : Module() {
|
|
|
562
646
|
|
|
563
647
|
// MARK: - Stream Commands
|
|
564
648
|
|
|
565
|
-
|
|
649
|
+
SdkAsyncFunction("startStream") { params: Map<String, Any> ->
|
|
566
650
|
requireSdk().startStream(StreamRequest.fromMap(params)).values
|
|
567
651
|
}
|
|
568
652
|
|
|
569
|
-
|
|
653
|
+
SdkAsyncFunction("startExternallyManagedStream") { params: Map<String, Any> ->
|
|
570
654
|
requireSdk().startExternallyManagedStream(StreamRequest.fromMap(params)).values
|
|
571
655
|
}
|
|
572
656
|
|
|
573
|
-
|
|
657
|
+
SdkAsyncFunction("stopStream") { -> requireSdk().stopStream().values }
|
|
574
658
|
|
|
575
|
-
|
|
659
|
+
SdkAsyncFunction("sendExternallyManagedStreamKeepAlive") { params: Map<String, Any> ->
|
|
576
660
|
sdk?.sendExternallyManagedStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
|
|
577
661
|
}
|
|
578
662
|
|
|
579
663
|
// MARK: - Microphone Commands
|
|
580
664
|
|
|
581
|
-
|
|
665
|
+
SdkAsyncFunction("setMicState") {
|
|
582
666
|
enabled: Boolean,
|
|
583
667
|
useGlassesMic: Boolean?,
|
|
584
668
|
sendTranscript: Boolean?,
|
|
@@ -600,7 +684,7 @@ class BluetoothSdkModule : Module() {
|
|
|
600
684
|
|
|
601
685
|
// MARK: - Audio Playback Monitoring
|
|
602
686
|
|
|
603
|
-
|
|
687
|
+
SdkAsyncFunction("setOwnAppAudioPlaying") { playing: Boolean ->
|
|
604
688
|
sdk?.setOwnAppAudioPlaying(playing)
|
|
605
689
|
}
|
|
606
690
|
|
|
@@ -619,7 +703,7 @@ class BluetoothSdkModule : Module() {
|
|
|
619
703
|
|
|
620
704
|
// MARK: - RGB LED Control
|
|
621
705
|
|
|
622
|
-
|
|
706
|
+
SdkAsyncFunction("rgbLedControl") {
|
|
623
707
|
requestId: String,
|
|
624
708
|
packageName: String?,
|
|
625
709
|
action: String,
|
|
@@ -16,6 +16,8 @@ import com.mentra.bluetoothsdk.services.ForegroundService
|
|
|
16
16
|
import com.mentra.bluetoothsdk.services.PhoneMic
|
|
17
17
|
import com.mentra.bluetoothsdk.sgcs.G1
|
|
18
18
|
import com.mentra.bluetoothsdk.sgcs.G2
|
|
19
|
+
import com.mentra.bluetoothsdk.sgcs.SceneElement
|
|
20
|
+
import com.mentra.bluetoothsdk.sgcs.SceneFrame
|
|
19
21
|
import com.mentra.bluetoothsdk.sgcs.Mach1
|
|
20
22
|
import com.mentra.bluetoothsdk.sgcs.MentraLive
|
|
21
23
|
import com.mentra.bluetoothsdk.sgcs.MentraNex
|
|
@@ -144,10 +146,6 @@ class DeviceManager {
|
|
|
144
146
|
get() = DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean ?: false
|
|
145
147
|
set(value) = DeviceStore.apply("bluetooth", "bypass_vad", value)
|
|
146
148
|
|
|
147
|
-
private var offlineCaptionsRunning: Boolean
|
|
148
|
-
get() = DeviceStore.store.get("bluetooth", "offline_captions_running") as? Boolean ?: false
|
|
149
|
-
set(value) = DeviceStore.apply("bluetooth", "offline_captions_running", value)
|
|
150
|
-
|
|
151
149
|
private var localSttFallbackActive: Boolean
|
|
152
150
|
get() = DeviceStore.store.get("bluetooth", "local_stt_fallback_active") as? Boolean ?: false
|
|
153
151
|
set(value) = DeviceStore.apply("bluetooth", "local_stt_fallback_active", value)
|
|
@@ -660,6 +658,13 @@ class DeviceManager {
|
|
|
660
658
|
var borderWidth: Int? = null,
|
|
661
659
|
var borderRadius: Int? = null
|
|
662
660
|
)
|
|
661
|
+
|
|
662
|
+
// Scene slots — one whole SceneFrame per view (main/dashboard), parallel to
|
|
663
|
+
// viewStates. When a slot holds a scene, viewStates carries a "scene"
|
|
664
|
+
// sentinel so sendCurrentState routes here. Holding the WHOLE frame keeps
|
|
665
|
+
// native re-dispatch coherent (dashboard exit re-applies a complete scene,
|
|
666
|
+
// not whatever element happened to arrive last).
|
|
667
|
+
private val sceneStates = arrayOfNulls<SceneFrame>(2)
|
|
663
668
|
// MARK: - End Unique
|
|
664
669
|
|
|
665
670
|
// MARK: - Voice Data Handling
|
|
@@ -740,7 +745,7 @@ class DeviceManager {
|
|
|
740
745
|
// class to fire `vad_status` (a separate signal the cloud SDK
|
|
741
746
|
// surfaces as `session.audio.isSpeaking`).
|
|
742
747
|
handleSendingPcm(pcmData)
|
|
743
|
-
if (shouldSendTranscript ||
|
|
748
|
+
if (shouldSendTranscript || localSttFallbackActive) {
|
|
744
749
|
if (ensureTranscriberInitialized()) {
|
|
745
750
|
transcriber?.acceptAudio(pcmData)
|
|
746
751
|
}
|
|
@@ -877,16 +882,8 @@ class DeviceManager {
|
|
|
877
882
|
}
|
|
878
883
|
|
|
879
884
|
// executor.execute {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
currentViewState = viewStates[1]
|
|
883
|
-
} else {
|
|
884
|
-
currentViewState = viewStates[0]
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
if (hUp && !contextualDashboard) {
|
|
888
|
-
currentViewState = viewStates[0]
|
|
889
|
-
}
|
|
885
|
+
val currentStateIndex = if (hUp && contextualDashboard) 1 else 0
|
|
886
|
+
val currentViewState: ViewState = viewStates[currentStateIndex]
|
|
890
887
|
|
|
891
888
|
if (sgc?.type?.contains(DeviceTypes.SIMULATED) == true) {
|
|
892
889
|
// dont send the event to glasses that aren't there:
|
|
@@ -944,6 +941,10 @@ class DeviceManager {
|
|
|
944
941
|
)
|
|
945
942
|
}
|
|
946
943
|
|
|
944
|
+
"scene" -> {
|
|
945
|
+
sceneStates[currentStateIndex]?.let { sgc?.applySceneFrame(it) }
|
|
946
|
+
}
|
|
947
|
+
|
|
947
948
|
"clear_view" -> sgc?.clearDisplay()
|
|
948
949
|
else -> Bridge.log("MAN: UNHANDLED LAYOUT_TYPE ${currentViewState.layoutType}")
|
|
949
950
|
}
|
|
@@ -1362,9 +1363,31 @@ class DeviceManager {
|
|
|
1362
1363
|
val isDashboard = view == "dashboard"
|
|
1363
1364
|
val stateIndex = if (isDashboard) 1 else 0
|
|
1364
1365
|
|
|
1366
|
+
// Scene frames (display.render() pipeline) take their own path: the
|
|
1367
|
+
// whole frame is the unit, not a layout, and the host's per-element
|
|
1368
|
+
// annotations make redundant frames self-deduping (all-"unchanged"
|
|
1369
|
+
// frames no-op in the SGC base handler).
|
|
1370
|
+
@Suppress("UNCHECKED_CAST") val sceneMap = event["scene"] as? Map<String, Any>
|
|
1371
|
+
if (sceneMap != null) {
|
|
1372
|
+
handleSceneEvent(stateIndex, sceneMap)
|
|
1373
|
+
return
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1365
1376
|
@Suppress("UNCHECKED_CAST") val layout = event["layout"] as? Map<String, Any> ?: return
|
|
1366
1377
|
|
|
1367
1378
|
val layoutType = layout["layoutType"] as? String
|
|
1379
|
+
|
|
1380
|
+
// Scene→legacy handoff: a legacy layout is about to draw over a scene
|
|
1381
|
+
// (e.g. a cloud app taking the view from a miniapp). Sweep the scene's
|
|
1382
|
+
// elements first so they don't linger under the new content; clear_view
|
|
1383
|
+
// wipes everything anyway.
|
|
1384
|
+
sceneStates[stateIndex]?.let { prevFrame ->
|
|
1385
|
+
sceneStates[stateIndex] = null
|
|
1386
|
+
if (layoutType != "clear_view") {
|
|
1387
|
+
sgc?.clearSceneElements(prevFrame.elements.map { it.id })
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1368
1391
|
val text = parsePlaceholders(layout.getString("text", " "))
|
|
1369
1392
|
val topText = parsePlaceholders(layout.getString("topText", " "))
|
|
1370
1393
|
val bottomText = parsePlaceholders(layout.getString("bottomText", " "))
|
|
@@ -1414,6 +1437,86 @@ class DeviceManager {
|
|
|
1414
1437
|
}
|
|
1415
1438
|
}
|
|
1416
1439
|
|
|
1440
|
+
/** Parse + store a scene frame, then dispatch it if its view is visible. */
|
|
1441
|
+
private fun handleSceneEvent(stateIndex: Int, sceneMap: Map<String, Any>) {
|
|
1442
|
+
var frame = parseSceneFrame(sceneMap) ?: return
|
|
1443
|
+
val prevFrame = sceneStates[stateIndex]
|
|
1444
|
+
|
|
1445
|
+
if (prevFrame == null) {
|
|
1446
|
+
// Legacy→scene handoff: stale legacy content (e.g. a cloud app's
|
|
1447
|
+
// text wall) must not linger under the scene's elements.
|
|
1448
|
+
// clearDisplay is the per-device "wipe what's there" (blank-in-place
|
|
1449
|
+
// on G2 — no page rebuild).
|
|
1450
|
+
val prevLegacyType = viewStates[stateIndex].layoutType
|
|
1451
|
+
if (prevLegacyType.isNotEmpty() && prevLegacyType != "clear_view" && prevLegacyType != "scene") {
|
|
1452
|
+
sgc?.clearDisplay()
|
|
1453
|
+
}
|
|
1454
|
+
} else if (prevFrame.appId != frame.appId) {
|
|
1455
|
+
// Cross-app switch: the host's diff baseline is per-app, so the new
|
|
1456
|
+
// app's annotations don't know the old app's elements are on the
|
|
1457
|
+
// glasses. Sweep the old app's elements (SGC registries still map
|
|
1458
|
+
// them), then paint the new frame from scratch. In practice the
|
|
1459
|
+
// boot message interposes between apps, so this isn't visible as a
|
|
1460
|
+
// blank.
|
|
1461
|
+
sgc?.clearSceneElements(prevFrame.elements.map { it.id })
|
|
1462
|
+
frame = frame.copy(replay = true, elements = frame.elements.map { it.copy(change = "created") })
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Store the REDISPATCH form: any later sendCurrentState (dashboard
|
|
1466
|
+
// exit, head-up return) must repaint the whole frame — the original
|
|
1467
|
+
// annotations are only valid for the first dispatch right now.
|
|
1468
|
+
sceneStates[stateIndex] =
|
|
1469
|
+
frame.copy(replay = true, elements = frame.elements.map { it.copy(change = "created") })
|
|
1470
|
+
viewStates[stateIndex] = ViewState(" ", " ", " ", "scene", " ", null, null)
|
|
1471
|
+
|
|
1472
|
+
val hUp = headUp && contextualDashboard
|
|
1473
|
+
if ((stateIndex == 0 && !hUp) || (stateIndex == 1 && hUp)) {
|
|
1474
|
+
dispatchSceneFrame(frame)
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/** Guarded scene dispatch — mirrors sendCurrentState's send conditions. */
|
|
1479
|
+
private fun dispatchSceneFrame(frame: SceneFrame) {
|
|
1480
|
+
if (screenDisabled) return
|
|
1481
|
+
if (sgc?.type?.contains(DeviceTypes.SIMULATED) == true) return
|
|
1482
|
+
if (sgc?.fullyBooted != true) {
|
|
1483
|
+
Bridge.log("MAN: dispatchSceneFrame(): sgc not ready")
|
|
1484
|
+
return
|
|
1485
|
+
}
|
|
1486
|
+
sgc?.applySceneFrame(frame)
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
@Suppress("UNCHECKED_CAST")
|
|
1490
|
+
private fun parseSceneFrame(sceneMap: Map<String, Any>): SceneFrame? {
|
|
1491
|
+
val elementsRaw = sceneMap["elements"] as? List<Map<String, Any>> ?: return null
|
|
1492
|
+
val elements =
|
|
1493
|
+
elementsRaw.mapNotNull { el ->
|
|
1494
|
+
val box = el["box"] as? Map<String, Any> ?: return@mapNotNull null
|
|
1495
|
+
val style = el["style"] as? Map<String, Any>
|
|
1496
|
+
SceneElement(
|
|
1497
|
+
id = el["id"] as? String ?: return@mapNotNull null,
|
|
1498
|
+
type = el["type"] as? String ?: return@mapNotNull null,
|
|
1499
|
+
x = (box["x"] as? Number)?.toInt() ?: 0,
|
|
1500
|
+
y = (box["y"] as? Number)?.toInt() ?: 0,
|
|
1501
|
+
w = (box["w"] as? Number)?.toInt() ?: 0,
|
|
1502
|
+
h = (box["h"] as? Number)?.toInt() ?: 0,
|
|
1503
|
+
text = (el["text"] as? String)?.let { parsePlaceholders(it) },
|
|
1504
|
+
data = el["data"] as? String,
|
|
1505
|
+
border = (style?.get("border") as? Number)?.toInt() ?: 0,
|
|
1506
|
+
radius = (style?.get("radius") as? Number)?.toInt() ?: 0,
|
|
1507
|
+
change = el["change"] as? String ?: "created",
|
|
1508
|
+
contentHash = el["contentHash"] as? String ?: ""
|
|
1509
|
+
)
|
|
1510
|
+
}
|
|
1511
|
+
return SceneFrame(
|
|
1512
|
+
appId = sceneMap["appId"] as? String ?: "",
|
|
1513
|
+
epoch = (sceneMap["sceneEpoch"] as? Number)?.toInt() ?: 0,
|
|
1514
|
+
replay = sceneMap["replay"] as? Boolean ?: false,
|
|
1515
|
+
elements = elements,
|
|
1516
|
+
removed = (sceneMap["removed"] as? List<*>)?.filterIsInstance<String>() ?: emptyList()
|
|
1517
|
+
)
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1417
1520
|
fun showDashboard() {
|
|
1418
1521
|
sgc?.showDashboard()
|
|
1419
1522
|
}
|
|
@@ -1669,7 +1772,7 @@ class DeviceManager {
|
|
|
1669
1772
|
|
|
1670
1773
|
fun setMicState() {
|
|
1671
1774
|
val willSendPcm = shouldSendPcm || shouldSendLc3
|
|
1672
|
-
val willSendTranscript = shouldSendTranscript ||
|
|
1775
|
+
val willSendTranscript = shouldSendTranscript || localSttFallbackActive
|
|
1673
1776
|
val nextEnabled = willSendPcm || willSendTranscript
|
|
1674
1777
|
// Tell VAD when the mic is shutting down so it doesn't get stuck in
|
|
1675
1778
|
// a stale "speaking" state and keep emitting vad_status=true after
|
|
@@ -1697,7 +1800,7 @@ class DeviceManager {
|
|
|
1697
1800
|
iso = manualIso,
|
|
1698
1801
|
)
|
|
1699
1802
|
Bridge.log(
|
|
1700
|
-
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=${routed.requestId} size=${routed.size.value} compress=${routed.compress.value} save=${routed.save} sound=${routed.sound} exposureTimeNs=$exposureNs iso=${manualIso ?: "auto"} aeDivisor=${routed.aeExposureDivisor} isoCap=${routed.isoCap} sgc=${sgc?.javaClass?.simpleName ?: "null"}"
|
|
1803
|
+
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=${routed.requestId} size=${routed.size.value} mode=${routed.mode.value} compress=${routed.compress.value} save=${routed.save} sound=${routed.sound} exposureTimeNs=$exposureNs iso=${manualIso ?: "auto"} aeDivisor=${routed.aeExposureDivisor} isoCap=${routed.isoCap} sgc=${sgc?.javaClass?.simpleName ?: "null"}"
|
|
1701
1804
|
)
|
|
1702
1805
|
val activeSgc = sgc
|
|
1703
1806
|
if (activeSgc == null) {
|
|
@@ -292,6 +292,11 @@ object DeviceStore {
|
|
|
292
292
|
"bluetooth" to "camera_fov" -> {
|
|
293
293
|
DeviceManager.getInstance().sgc?.sendCameraFovSetting()
|
|
294
294
|
}
|
|
295
|
+
"bluetooth" to "button_video_settings" -> {
|
|
296
|
+
DeviceManager.getInstance().sgc?.sendButtonVideoRecordingSettings()
|
|
297
|
+
}
|
|
298
|
+
// Legacy scalar keys remain supported for older hosts. New code should write the
|
|
299
|
+
// canonical button_video_settings object so width/height/fps update atomically.
|
|
295
300
|
"bluetooth" to "button_video_width",
|
|
296
301
|
"bluetooth" to "button_video_height",
|
|
297
302
|
"bluetooth" to "button_video_fps" -> {
|
|
@@ -303,12 +308,6 @@ object DeviceStore {
|
|
|
303
308
|
DeviceManager.getInstance().setMicState()
|
|
304
309
|
}
|
|
305
310
|
}
|
|
306
|
-
"bluetooth" to "offline_captions_running" -> {
|
|
307
|
-
(value as? Boolean)?.let { running ->
|
|
308
|
-
Bridge.log("DeviceStore: offline_captions_running changed to $running")
|
|
309
|
-
DeviceManager.getInstance().setMicState()
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
311
|
"bluetooth" to "local_stt_fallback_active" -> {
|
|
313
312
|
(value as? Boolean)?.let { active ->
|
|
314
313
|
Bridge.log("DeviceStore: local_stt_fallback_active changed to $active")
|