@mentra/bluetooth-sdk 0.1.8 → 0.1.10
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 +116 -18
- package/android/build.gradle +289 -16
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/build.gradle +95 -6
- package/android/settings.gradle +3 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +42 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/build/BluetoothSdk.types.d.ts +241 -43
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +11 -10
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +36 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +30 -52
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +4 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +25 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +23 -0
- package/build/index.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +2 -2
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +3 -3
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +174 -70
- package/ios/LocalPhotoUploadServer.swift +370 -105
- package/ios/MentraBluetoothSDK.podspec +3 -1
- package/ios/MentraPhotoReceiverModule.swift +62 -10
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -0
- package/ios/Source/Bridge.swift +58 -19
- package/ios/Source/DeviceManager.swift +149 -17
- package/ios/Source/DeviceStore.swift +16 -6
- package/ios/Source/MentraBluetoothSDK.swift +803 -47
- package/ios/Source/ObservableStore.swift +8 -2
- package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
- package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
- package/ios/Source/controllers/ControllerManager.swift +4 -3
- package/ios/Source/controllers/R1.swift +3 -3
- package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
- package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
- package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
- package/ios/Source/services/PhoneMic.swift +1 -0
- package/ios/Source/sgcs/G1.swift +2 -2
- package/ios/Source/sgcs/G2.swift +645 -403
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +812 -319
- package/ios/Source/sgcs/MentraNex.swift +412 -182
- package/ios/Source/sgcs/SGCManager.swift +57 -3
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
- package/ios/Source/status/DeviceStatus.swift +974 -0
- package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
- package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
- package/ios/Source/stt/STTTools.swift +27 -12
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
- package/ios/Source/tts/TTSTools.swift +164 -0
- package/ios/Source/utils/JSCExperiment.swift +7 -7
- package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
- package/ios/Source/utils/MessageChunker.swift +78 -21
- package/ios/Source/utils/TarBz2Extractor.swift +53 -2
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +68 -4
- package/src/BluetoothSdk.types.ts +331 -59
- package/src/_private/BluetoothSdkModule.ts +113 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +58 -15
- package/src/react/useMentraBluetooth.ts +6 -4
- package/android/.project +0 -28
- package/android/lc3Lib/.project +0 -28
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Source/Status/DeviceStatus.swift +0 -471
- /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
- /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
- /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
- /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
- /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
|
@@ -132,6 +132,13 @@ class DeviceManager {
|
|
|
132
132
|
get() = DeviceStore.store.get("bluetooth", "power_saving_mode") as? Boolean ?: false
|
|
133
133
|
set(value) = DeviceStore.apply("bluetooth", "power_saving_mode", value)
|
|
134
134
|
|
|
135
|
+
// Phone-side VAD gating switch. Default is OFF (VAD runs) so the
|
|
136
|
+
// coordinator can drive per-utterance offline/online STT switching from
|
|
137
|
+
// `vad_status` events. Set to `true` only as an emergency kill-switch.
|
|
138
|
+
private var bypassVad: Boolean
|
|
139
|
+
get() = DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean ?: false
|
|
140
|
+
set(value) = DeviceStore.apply("bluetooth", "bypass_vad", value)
|
|
141
|
+
|
|
135
142
|
private var offlineCaptionsRunning: Boolean
|
|
136
143
|
get() = DeviceStore.store.get("bluetooth", "offline_captions_running") as? Boolean ?: false
|
|
137
144
|
set(value) = DeviceStore.apply("bluetooth", "offline_captions_running", value)
|
|
@@ -192,6 +199,7 @@ class DeviceManager {
|
|
|
192
199
|
// Guard against duplicate ready callbacks firing back-to-back.
|
|
193
200
|
private var lastReadyHandledAtMs: Long = 0L
|
|
194
201
|
private var lastReadyHandledKey: String = ""
|
|
202
|
+
private var lastSystemTimeSyncConnectionKey: String = ""
|
|
195
203
|
|
|
196
204
|
private var systemMicUnavailable: Boolean
|
|
197
205
|
get() = DeviceStore.store.get("bluetooth", "systemMicUnavailable") as? Boolean ?: false
|
|
@@ -237,6 +245,11 @@ class DeviceManager {
|
|
|
237
245
|
private var lastLc3Event: Long? = null
|
|
238
246
|
private var micReinitRunnable: Runnable? = null
|
|
239
247
|
|
|
248
|
+
// VAD
|
|
249
|
+
private val vadBuffer = mutableListOf<ByteArray>()
|
|
250
|
+
private var isSpeaking = false
|
|
251
|
+
private var vadPolicy: com.mentra.bluetoothsdk.stt.VadGateSpeechPolicy? = null
|
|
252
|
+
|
|
240
253
|
// STT
|
|
241
254
|
private var transcriber: SherpaOnnxTranscriber? = null
|
|
242
255
|
|
|
@@ -272,6 +285,9 @@ class DeviceManager {
|
|
|
272
285
|
} catch (e: Exception) {
|
|
273
286
|
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
274
287
|
transcriber = null
|
|
288
|
+
} catch (e: LinkageError) {
|
|
289
|
+
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
290
|
+
transcriber = null
|
|
275
291
|
}
|
|
276
292
|
|
|
277
293
|
// Initialize LC3 encoder/decoder for unified audio encoding
|
|
@@ -286,6 +302,30 @@ class DeviceManager {
|
|
|
286
302
|
lc3DecoderPtr = 0
|
|
287
303
|
}
|
|
288
304
|
|
|
305
|
+
// Initialize phone-side Silero VAD. Used by handlePcm to gate audio
|
|
306
|
+
// fan-out and to drive per-utterance offline/online STT switching
|
|
307
|
+
// (see LocalSttFallbackCoordinator on the JS side). If a previous
|
|
308
|
+
// policy somehow exists (re-init on hot reload), stop it first to
|
|
309
|
+
// release the ONNX session.
|
|
310
|
+
try {
|
|
311
|
+
vadPolicy?.stop()
|
|
312
|
+
val ctx = Bridge.getContext()
|
|
313
|
+
val policy = com.mentra.bluetoothsdk.stt.VadGateSpeechPolicy(ctx)
|
|
314
|
+
policy.init(blockSizeSamples = 512)
|
|
315
|
+
policy.onSpeechStateChanged = { speaking ->
|
|
316
|
+
isSpeaking = speaking
|
|
317
|
+
Bridge.sendVoiceActivityDetectionStatus(speaking)
|
|
318
|
+
}
|
|
319
|
+
vadPolicy = policy
|
|
320
|
+
Bridge.log("VadGateSpeechPolicy initialized")
|
|
321
|
+
} catch (e: Exception) {
|
|
322
|
+
Bridge.log("Failed to initialize VadGateSpeechPolicy: ${e.message}")
|
|
323
|
+
vadPolicy = null
|
|
324
|
+
} catch (e: LinkageError) {
|
|
325
|
+
Bridge.log("Failed to initialize VadGateSpeechPolicy: ${e.message}")
|
|
326
|
+
vadPolicy = null
|
|
327
|
+
}
|
|
328
|
+
|
|
289
329
|
// Mic reinit check every 10 seconds
|
|
290
330
|
val micReinitR =
|
|
291
331
|
object : Runnable {
|
|
@@ -590,7 +630,12 @@ class DeviceManager {
|
|
|
590
630
|
var layoutType: String,
|
|
591
631
|
var text: String,
|
|
592
632
|
var data: String?,
|
|
593
|
-
var animationData: Map<String, Any
|
|
633
|
+
var animationData: Map<String, Any>?,
|
|
634
|
+
// Optional bitmap_view container position/size (used by G2; ignored by others)
|
|
635
|
+
var bmpX: Int? = null,
|
|
636
|
+
var bmpY: Int? = null,
|
|
637
|
+
var bmpWidth: Int? = null,
|
|
638
|
+
var bmpHeight: Int? = null
|
|
594
639
|
)
|
|
595
640
|
// MARK: - End Unique
|
|
596
641
|
|
|
@@ -652,9 +697,17 @@ class DeviceManager {
|
|
|
652
697
|
}
|
|
653
698
|
|
|
654
699
|
fun handlePcm(pcmData: ByteArray) {
|
|
700
|
+
// Audio always flows. The previous phone-side Silero VAD gate was a
|
|
701
|
+
// bandwidth-saver that ate transcripts when the mic delivered frames
|
|
702
|
+
// not aligned to 512 samples (the case for Android AudioRecord on the
|
|
703
|
+
// phone internal mic) and was never wired up correctly anyway —
|
|
704
|
+
// `bypass_vad_for_debugging` was dead, cloud-side `bypass_vad` was
|
|
705
|
+
// the only knob, and the policy double-VAD'd what the cloud already
|
|
706
|
+
// VADs server-side. VadGateSpeechPolicy is kept around because
|
|
707
|
+
// hardware-side VAD events from the glasses route through the same
|
|
708
|
+
// class to fire `vad_status` (a separate signal the cloud SDK
|
|
709
|
+
// surfaces as `session.audio.isSpeaking`).
|
|
655
710
|
handleSendingPcm(pcmData)
|
|
656
|
-
|
|
657
|
-
// Send PCM to local transcriber (always needs raw PCM)
|
|
658
711
|
if (shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive) {
|
|
659
712
|
transcriber?.acceptAudio(pcmData)
|
|
660
713
|
}
|
|
@@ -831,7 +884,15 @@ class DeviceManager {
|
|
|
831
884
|
sgc?.sendTextWall("${currentViewState.title}\n\n${currentViewState.text}")
|
|
832
885
|
}
|
|
833
886
|
"bitmap_view" -> {
|
|
834
|
-
currentViewState.data?.let { data ->
|
|
887
|
+
currentViewState.data?.let { data ->
|
|
888
|
+
sgc?.displayBitmap(
|
|
889
|
+
data,
|
|
890
|
+
currentViewState.bmpX,
|
|
891
|
+
currentViewState.bmpY,
|
|
892
|
+
currentViewState.bmpWidth,
|
|
893
|
+
currentViewState.bmpHeight
|
|
894
|
+
)
|
|
895
|
+
}
|
|
835
896
|
}
|
|
836
897
|
"clear_view" -> sgc?.clearDisplay()
|
|
837
898
|
else -> Bridge.log("MAN: UNHANDLED LAYOUT_TYPE ${currentViewState.layoutType}")
|
|
@@ -974,6 +1035,7 @@ class DeviceManager {
|
|
|
974
1035
|
Bridge.log("MAN: Cleaning up previous sgc type: ${sgc?.type}")
|
|
975
1036
|
sgc?.cleanup()
|
|
976
1037
|
sgc = null
|
|
1038
|
+
lastSystemTimeSyncConnectionKey = ""
|
|
977
1039
|
}
|
|
978
1040
|
|
|
979
1041
|
if (sgc != null) {
|
|
@@ -992,10 +1054,9 @@ class DeviceManager {
|
|
|
992
1054
|
} else if (wearable.contains(DeviceTypes.NEX)) {
|
|
993
1055
|
sgc = MentraNex()
|
|
994
1056
|
} else if (wearable.contains(DeviceTypes.MACH1)) {
|
|
995
|
-
sgc =
|
|
1057
|
+
sgc = createOptionalMach1Sgc(DeviceTypes.MACH1)
|
|
996
1058
|
} else if (wearable.contains(DeviceTypes.Z100)) {
|
|
997
|
-
sgc =
|
|
998
|
-
sgc?.type = DeviceTypes.Z100 // Override type to Z100
|
|
1059
|
+
sgc = createOptionalMach1Sgc(DeviceTypes.Z100)
|
|
999
1060
|
} else if (wearable.contains(DeviceTypes.FRAME)) {
|
|
1000
1061
|
// sgc = FrameManager()
|
|
1001
1062
|
}
|
|
@@ -1003,6 +1064,19 @@ class DeviceManager {
|
|
|
1003
1064
|
DeviceStore.apply("glasses", "deviceModel", sgc?.type ?: "")
|
|
1004
1065
|
}
|
|
1005
1066
|
|
|
1067
|
+
private fun createOptionalMach1Sgc(deviceType: String): SGCManager? {
|
|
1068
|
+
return try {
|
|
1069
|
+
Mach1().also { sgc ->
|
|
1070
|
+
if (deviceType == DeviceTypes.Z100) {
|
|
1071
|
+
sgc.type = DeviceTypes.Z100
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
} catch (e: LinkageError) {
|
|
1075
|
+
Bridge.log("Failed to initialize $deviceType support: ${e.message}")
|
|
1076
|
+
null
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1006
1080
|
fun initController(controllerType: String) {
|
|
1007
1081
|
Bridge.log("MAN: Initializing controller: $controllerType")
|
|
1008
1082
|
if (controller != null && controller?.type != controllerType) {
|
|
@@ -1048,6 +1122,8 @@ class DeviceManager {
|
|
|
1048
1122
|
defaultWearable = sgc?.type ?: ""
|
|
1049
1123
|
searching = false
|
|
1050
1124
|
|
|
1125
|
+
syncSystemTimeOnceForConnection(readyKey)
|
|
1126
|
+
|
|
1051
1127
|
// Apply dashboard position before any boot text so content doesn't jump.
|
|
1052
1128
|
sgc?.setDashboardPosition(dashboardHeight, dashboardDepth)
|
|
1053
1129
|
|
|
@@ -1084,6 +1160,21 @@ class DeviceManager {
|
|
|
1084
1160
|
Bridge.saveSetting("device_address", deviceAddress)
|
|
1085
1161
|
}
|
|
1086
1162
|
|
|
1163
|
+
private fun syncSystemTimeOnceForConnection(connectionKey: String) {
|
|
1164
|
+
val activeSgc = sgc ?: return
|
|
1165
|
+
if (activeSgc.type.contains(DeviceTypes.SIMULATED)) {
|
|
1166
|
+
return
|
|
1167
|
+
}
|
|
1168
|
+
if (connectionKey == lastSystemTimeSyncConnectionKey) {
|
|
1169
|
+
return
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
lastSystemTimeSyncConnectionKey = connectionKey
|
|
1173
|
+
val timestampMs = System.currentTimeMillis()
|
|
1174
|
+
Bridge.log("MAN: Syncing glasses system time once for connection: $timestampMs")
|
|
1175
|
+
activeSgc.sendSetSystemTime(timestampMs)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1087
1178
|
private fun handleG1Ready() {
|
|
1088
1179
|
// G1-specific setup (if any needed in the future)
|
|
1089
1180
|
// Note: G1-specific settings like silent mode, battery status,
|
|
@@ -1096,8 +1187,9 @@ class DeviceManager {
|
|
|
1096
1187
|
|
|
1097
1188
|
fun handleDeviceDisconnected() {
|
|
1098
1189
|
Bridge.log("MAN: Device disconnected")
|
|
1190
|
+
lastSystemTimeSyncConnectionKey = ""
|
|
1099
1191
|
DeviceStore.apply("glasses", "headUp", false)
|
|
1100
|
-
DeviceStore.apply("glasses", "voiceActivityDetectionEnabled",
|
|
1192
|
+
DeviceStore.apply("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
1101
1193
|
}
|
|
1102
1194
|
|
|
1103
1195
|
fun handleControllerReady() {
|
|
@@ -1154,7 +1246,26 @@ class DeviceManager {
|
|
|
1154
1246
|
val title = parsePlaceholders(layout.getString("title", " "))
|
|
1155
1247
|
val data = layout["data"] as? String
|
|
1156
1248
|
|
|
1157
|
-
|
|
1249
|
+
// Optional bitmap_view container position/size (forwarded to the SGC; used by G2).
|
|
1250
|
+
val bmpX = (layout["x"] as? Number)?.toInt()
|
|
1251
|
+
val bmpY = (layout["y"] as? Number)?.toInt()
|
|
1252
|
+
val bmpWidth = (layout["width"] as? Number)?.toInt()
|
|
1253
|
+
val bmpHeight = (layout["height"] as? Number)?.toInt()
|
|
1254
|
+
|
|
1255
|
+
var newViewState =
|
|
1256
|
+
ViewState(
|
|
1257
|
+
topText,
|
|
1258
|
+
bottomText,
|
|
1259
|
+
title,
|
|
1260
|
+
layoutType ?: "",
|
|
1261
|
+
text,
|
|
1262
|
+
data,
|
|
1263
|
+
null,
|
|
1264
|
+
bmpX,
|
|
1265
|
+
bmpY,
|
|
1266
|
+
bmpWidth,
|
|
1267
|
+
bmpHeight
|
|
1268
|
+
)
|
|
1158
1269
|
|
|
1159
1270
|
val currentState = viewStates[stateIndex]
|
|
1160
1271
|
|
|
@@ -1176,6 +1287,10 @@ class DeviceManager {
|
|
|
1176
1287
|
sgc?.showDashboard()
|
|
1177
1288
|
}
|
|
1178
1289
|
|
|
1290
|
+
fun showNotificationsPanel() {
|
|
1291
|
+
sgc?.showNotificationsPanel()
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1179
1294
|
fun ping() {
|
|
1180
1295
|
sgc?.ping()
|
|
1181
1296
|
}
|
|
@@ -1232,6 +1347,11 @@ class DeviceManager {
|
|
|
1232
1347
|
sgc?.sendHotspotState(enabled)
|
|
1233
1348
|
}
|
|
1234
1349
|
|
|
1350
|
+
fun setSystemTime(timestampMs: Long) {
|
|
1351
|
+
Bridge.log("MAN: Setting glasses system time: $timestampMs")
|
|
1352
|
+
sgc?.sendSetSystemTime(timestampMs)
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1235
1355
|
fun queryGalleryStatus() {
|
|
1236
1356
|
Bridge.log("MAN: Querying gallery status from glasses")
|
|
1237
1357
|
sgc?.queryGalleryStatus()
|
|
@@ -1251,6 +1371,41 @@ class DeviceManager {
|
|
|
1251
1371
|
(sgc as? MentraLive)?.sendOtaQueryStatus()
|
|
1252
1372
|
}
|
|
1253
1373
|
|
|
1374
|
+
fun sendGalleryMode(requestId: String, enabled: Boolean) {
|
|
1375
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1376
|
+
live.sendGalleryMode(requestId, enabled)
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
fun sendButtonPhotoSettings(requestId: String, size: String) {
|
|
1380
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1381
|
+
live.sendButtonPhotoSettings(requestId, size)
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
fun sendButtonVideoRecordingSettings(requestId: String, width: Int, height: Int, fps: Int) {
|
|
1385
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1386
|
+
live.sendButtonVideoRecordingSettings(requestId, width, height, fps)
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
fun sendButtonCameraLedSetting(requestId: String, enabled: Boolean) {
|
|
1390
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1391
|
+
live.sendButtonCameraLedSetting(requestId, enabled)
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
fun sendButtonMaxRecordingTime(requestId: String, minutes: Int) {
|
|
1395
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1396
|
+
live.sendButtonMaxRecordingTime(requestId, minutes)
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
fun sendCameraFovSetting(requestId: String, fov: Int, roiPosition: Int) {
|
|
1400
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1401
|
+
live.sendCameraFovSetting(requestId, fov, roiPosition)
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
fun retryOtaVersionCheck() {
|
|
1405
|
+
Bridge.log("MAN: ⏰ Retrying glasses OTA version check after clock sync")
|
|
1406
|
+
(sgc as? MentraLive)?.sendOtaRetryVersionCheck()
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1254
1409
|
/**
|
|
1255
1410
|
* Read glasses media step volume (0–15) via K900 on Mentra Live only. Blocks until response,
|
|
1256
1411
|
* error, or timeout (used from JS AsyncFunction on a worker thread).
|
|
@@ -1324,11 +1479,19 @@ class DeviceManager {
|
|
|
1324
1479
|
sgc?.sendReboot()
|
|
1325
1480
|
}
|
|
1326
1481
|
|
|
1327
|
-
fun startVideoRecording(
|
|
1482
|
+
fun startVideoRecording(
|
|
1483
|
+
requestId: String,
|
|
1484
|
+
save: Boolean,
|
|
1485
|
+
sound: Boolean,
|
|
1486
|
+
width: Int = 0,
|
|
1487
|
+
height: Int = 0,
|
|
1488
|
+
fps: Int = 0,
|
|
1489
|
+
) {
|
|
1328
1490
|
Bridge.log(
|
|
1329
|
-
"MAN: onStartVideoRecording: requestId=$requestId, save=$save, flash=true, sound=$sound"
|
|
1491
|
+
"MAN: onStartVideoRecording: requestId=$requestId, save=$save, flash=true, sound=$sound, " +
|
|
1492
|
+
"resolution=${width}x${height}@${fps}fps"
|
|
1330
1493
|
)
|
|
1331
|
-
sgc?.startVideoRecording(requestId, save, true, sound)
|
|
1494
|
+
sgc?.startVideoRecording(requestId, save, true, sound, width, height, fps)
|
|
1332
1495
|
}
|
|
1333
1496
|
|
|
1334
1497
|
fun stopVideoRecording(requestId: String) {
|
|
@@ -1339,7 +1502,15 @@ class DeviceManager {
|
|
|
1339
1502
|
fun setMicState() {
|
|
1340
1503
|
val willSendPcm = shouldSendPcm || shouldSendLc3
|
|
1341
1504
|
val willSendTranscript = shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive
|
|
1342
|
-
|
|
1505
|
+
val nextEnabled = willSendPcm || willSendTranscript
|
|
1506
|
+
// Tell VAD when the mic is shutting down so it doesn't get stuck in
|
|
1507
|
+
// a stale "speaking" state and keep emitting vad_status=true after
|
|
1508
|
+
// audio stops flowing.
|
|
1509
|
+
if (micEnabled && !nextEnabled) {
|
|
1510
|
+
vadPolicy?.microphoneStateChanged(false)
|
|
1511
|
+
}
|
|
1512
|
+
micEnabled = nextEnabled
|
|
1513
|
+
vadBuffer.clear()
|
|
1343
1514
|
updateMicState()
|
|
1344
1515
|
}
|
|
1345
1516
|
|
|
@@ -1351,6 +1522,7 @@ class DeviceManager {
|
|
|
1351
1522
|
authToken: String?,
|
|
1352
1523
|
compress: String,
|
|
1353
1524
|
flash: Boolean,
|
|
1525
|
+
save: Boolean,
|
|
1354
1526
|
sound: Boolean,
|
|
1355
1527
|
exposureTimeNs: Double? = null,
|
|
1356
1528
|
iso: Int? = null,
|
|
@@ -1364,7 +1536,7 @@ class DeviceManager {
|
|
|
1364
1536
|
}
|
|
1365
1537
|
val manualIso = if (exposureNs != null) iso?.takeIf { it > 0 } else null
|
|
1366
1538
|
Bridge.log(
|
|
1367
|
-
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=$requestId appId=$appId size=$size compress=$compress flash=$flash sound=$sound exposureTimeNs=$exposureNs iso=${manualIso ?: "auto"} sgc=${sgc?.javaClass?.simpleName ?: "null"}"
|
|
1539
|
+
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=$requestId appId=$appId size=$size compress=$compress flash=$flash save=$save sound=$sound exposureTimeNs=$exposureNs iso=${manualIso ?: "auto"} sgc=${sgc?.javaClass?.simpleName ?: "null"}"
|
|
1368
1540
|
)
|
|
1369
1541
|
val activeSgc = sgc
|
|
1370
1542
|
if (activeSgc == null) {
|
|
@@ -1373,7 +1545,7 @@ class DeviceManager {
|
|
|
1373
1545
|
)
|
|
1374
1546
|
return
|
|
1375
1547
|
}
|
|
1376
|
-
activeSgc.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, sound, exposureNs, manualIso)
|
|
1548
|
+
activeSgc.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, save, sound, exposureNs, manualIso)
|
|
1377
1549
|
}
|
|
1378
1550
|
|
|
1379
1551
|
fun rgbLedControl(
|
|
@@ -1495,6 +1667,7 @@ class DeviceManager {
|
|
|
1495
1667
|
sgc?.clearDisplay()
|
|
1496
1668
|
sgc?.disconnect()
|
|
1497
1669
|
sgc = null // Clear the SGC reference after disconnect
|
|
1670
|
+
lastSystemTimeSyncConnectionKey = ""
|
|
1498
1671
|
searching = false
|
|
1499
1672
|
micEnabled = false
|
|
1500
1673
|
updateMicState()
|
|
@@ -1503,7 +1676,7 @@ class DeviceManager {
|
|
|
1503
1676
|
DeviceStore.apply("glasses", "deviceModel", "")
|
|
1504
1677
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
1505
1678
|
DeviceStore.apply("glasses", "connected", false)
|
|
1506
|
-
DeviceStore.apply("glasses", "voiceActivityDetectionEnabled",
|
|
1679
|
+
DeviceStore.apply("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
1507
1680
|
// disconnect the controller as well:
|
|
1508
1681
|
searchingController = false
|
|
1509
1682
|
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
@@ -66,7 +66,7 @@ object DeviceStore {
|
|
|
66
66
|
store.set("glasses", "deviceModel", "")
|
|
67
67
|
store.set("glasses", "firmwareVersion", "")
|
|
68
68
|
store.set("glasses", "micEnabled", false)
|
|
69
|
-
store.set("glasses", "voiceActivityDetectionEnabled",
|
|
69
|
+
store.set("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
70
70
|
store.set("glasses", "bluetoothClassicConnected", false)
|
|
71
71
|
store.set("glasses", "caseRemoved", true)
|
|
72
72
|
store.set("glasses", "caseOpen", true)
|
|
@@ -123,7 +123,7 @@ object DeviceStore {
|
|
|
123
123
|
store.set("bluetooth", "head_up_angle", 30)
|
|
124
124
|
store.set("bluetooth", "contextual_dashboard", true)
|
|
125
125
|
store.set("bluetooth", "gallery_mode", true)
|
|
126
|
-
store.set("bluetooth", "voice_activity_detection_enabled",
|
|
126
|
+
store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
127
127
|
store.set("bluetooth", "screen_disabled", false)
|
|
128
128
|
store.set("bluetooth", "button_photo_size", "medium")
|
|
129
129
|
store.set("bluetooth", "button_camera_led", true)
|
|
@@ -140,6 +140,9 @@ object DeviceStore {
|
|
|
140
140
|
store.set("bluetooth", "should_send_lc3", false)
|
|
141
141
|
store.set("bluetooth", "should_send_transcript", false)
|
|
142
142
|
store.set("bluetooth", "use_native_dashboard", false)
|
|
143
|
+
// Mentra Nex feature flags (off by default; toggled from Nex Developer Settings):
|
|
144
|
+
store.set("bluetooth", "nex_chinese_captions", false)
|
|
145
|
+
store.set("bluetooth", "nex_audio_playback", false)
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
fun get(category: String, key: String): Any? {
|
|
@@ -241,12 +244,29 @@ object DeviceStore {
|
|
|
241
244
|
DeviceManager.getInstance().sgc?.setDashboardMenu(items)
|
|
242
245
|
}
|
|
243
246
|
}
|
|
247
|
+
// `core` category is normalized to `bluetooth` before reaching this switch.
|
|
248
|
+
"bluetooth" to "calendar_events" -> {
|
|
249
|
+
@Suppress("UNCHECKED_CAST")
|
|
250
|
+
(value as? List<Map<String, Any>>)?.let { events ->
|
|
251
|
+
DeviceManager.getInstance().sgc?.sendCalendarEvents(events)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
"bluetooth" to "metric_system",
|
|
255
|
+
"bluetooth" to "twelve_hour_time" -> {
|
|
256
|
+
DeviceManager.getInstance().sgc?.sendDashboardDisplaySettings()
|
|
257
|
+
}
|
|
244
258
|
"bluetooth" to "gallery_mode" -> {
|
|
245
259
|
DeviceManager.getInstance().sgc?.sendGalleryMode()
|
|
246
260
|
}
|
|
247
261
|
"bluetooth" to "voice_activity_detection_enabled" -> {
|
|
248
262
|
DeviceManager.getInstance().sgc?.sendVoiceActivityDetectionSetting()
|
|
249
263
|
}
|
|
264
|
+
"bluetooth" to "nex_audio_playback" -> {
|
|
265
|
+
(value as? Boolean)?.let { enabled ->
|
|
266
|
+
Bridge.log("DeviceStore: nex_audio_playback changed to $enabled")
|
|
267
|
+
DeviceManager.getInstance().sgc?.applyNexAudioPlaybackSetting()
|
|
268
|
+
}
|
|
269
|
+
}
|
|
250
270
|
"bluetooth" to "screen_disabled" -> {
|
|
251
271
|
(value as? Boolean)?.let { disabled ->
|
|
252
272
|
if (disabled) {
|