@mentra/bluetooth-sdk 0.1.9 → 0.1.11
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 +195 -34
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +103 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +210 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +30 -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 +125 -11
- 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 +1293 -758
- 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 +197 -149
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +64 -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 +253 -44
- 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 +37 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +43 -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 +25 -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 +177 -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 +68 -19
- package/ios/Source/DeviceManager.swift +167 -32
- package/ios/Source/DeviceStore.swift +24 -8
- package/ios/Source/MentraBluetoothSDK.swift +803 -47
- package/ios/Source/ObservableStore.swift +6 -0
- package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
- package/ios/Source/{Camera → camera}/CameraModels.swift +226 -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 +5 -5
- package/ios/Source/sgcs/G2.swift +1494 -870
- package/ios/Source/sgcs/Mach1.swift +4 -4
- package/ios/Source/sgcs/MentraLive.swift +814 -321
- package/ios/Source/sgcs/MentraNex.swift +604 -244
- package/ios/Source/sgcs/SGCManager.swift +68 -5
- package/ios/Source/sgcs/Simulated.swift +9 -5
- 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 +344 -60
- package/src/_private/BluetoothSdkModule.ts +132 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +61 -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
|
@@ -27,6 +27,9 @@ import com.mentra.bluetoothsdk.utils.MicMap
|
|
|
27
27
|
import com.mentra.bluetoothsdk.utils.MicTypes
|
|
28
28
|
import com.mentra.lc3Lib.Lc3Cpp
|
|
29
29
|
import com.mentra.bluetoothsdk.stt.SherpaOnnxTranscriber
|
|
30
|
+
import kotlinx.coroutines.CoroutineScope
|
|
31
|
+
import kotlinx.coroutines.Dispatchers
|
|
32
|
+
import kotlinx.coroutines.launch
|
|
30
33
|
import java.text.SimpleDateFormat
|
|
31
34
|
import java.util.*
|
|
32
35
|
import java.util.concurrent.CountDownLatch
|
|
@@ -132,6 +135,13 @@ class DeviceManager {
|
|
|
132
135
|
get() = DeviceStore.store.get("bluetooth", "power_saving_mode") as? Boolean ?: false
|
|
133
136
|
set(value) = DeviceStore.apply("bluetooth", "power_saving_mode", value)
|
|
134
137
|
|
|
138
|
+
// Phone-side VAD gating switch. Default is OFF (VAD runs) so the
|
|
139
|
+
// coordinator can drive per-utterance offline/online STT switching from
|
|
140
|
+
// `vad_status` events. Set to `true` only as an emergency kill-switch.
|
|
141
|
+
private var bypassVad: Boolean
|
|
142
|
+
get() = DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean ?: false
|
|
143
|
+
set(value) = DeviceStore.apply("bluetooth", "bypass_vad", value)
|
|
144
|
+
|
|
135
145
|
private var offlineCaptionsRunning: Boolean
|
|
136
146
|
get() = DeviceStore.store.get("bluetooth", "offline_captions_running") as? Boolean ?: false
|
|
137
147
|
set(value) = DeviceStore.apply("bluetooth", "offline_captions_running", value)
|
|
@@ -192,6 +202,7 @@ class DeviceManager {
|
|
|
192
202
|
// Guard against duplicate ready callbacks firing back-to-back.
|
|
193
203
|
private var lastReadyHandledAtMs: Long = 0L
|
|
194
204
|
private var lastReadyHandledKey: String = ""
|
|
205
|
+
private var lastSystemTimeSyncConnectionKey: String = ""
|
|
195
206
|
|
|
196
207
|
private var systemMicUnavailable: Boolean
|
|
197
208
|
get() = DeviceStore.store.get("bluetooth", "systemMicUnavailable") as? Boolean ?: false
|
|
@@ -237,6 +248,11 @@ class DeviceManager {
|
|
|
237
248
|
private var lastLc3Event: Long? = null
|
|
238
249
|
private var micReinitRunnable: Runnable? = null
|
|
239
250
|
|
|
251
|
+
// VAD
|
|
252
|
+
private val vadBuffer = mutableListOf<ByteArray>()
|
|
253
|
+
private var isSpeaking = false
|
|
254
|
+
private var vadPolicy: com.mentra.bluetoothsdk.stt.VadGateSpeechPolicy? = null
|
|
255
|
+
|
|
240
256
|
// STT
|
|
241
257
|
private var transcriber: SherpaOnnxTranscriber? = null
|
|
242
258
|
|
|
@@ -272,6 +288,9 @@ class DeviceManager {
|
|
|
272
288
|
} catch (e: Exception) {
|
|
273
289
|
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
274
290
|
transcriber = null
|
|
291
|
+
} catch (e: LinkageError) {
|
|
292
|
+
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
293
|
+
transcriber = null
|
|
275
294
|
}
|
|
276
295
|
|
|
277
296
|
// Initialize LC3 encoder/decoder for unified audio encoding
|
|
@@ -286,6 +305,30 @@ class DeviceManager {
|
|
|
286
305
|
lc3DecoderPtr = 0
|
|
287
306
|
}
|
|
288
307
|
|
|
308
|
+
// Initialize phone-side Silero VAD. Used by handlePcm to gate audio
|
|
309
|
+
// fan-out and to drive per-utterance offline/online STT switching
|
|
310
|
+
// (see LocalSttFallbackCoordinator on the JS side). If a previous
|
|
311
|
+
// policy somehow exists (re-init on hot reload), stop it first to
|
|
312
|
+
// release the ONNX session.
|
|
313
|
+
try {
|
|
314
|
+
vadPolicy?.stop()
|
|
315
|
+
val ctx = Bridge.getContext()
|
|
316
|
+
val policy = com.mentra.bluetoothsdk.stt.VadGateSpeechPolicy(ctx)
|
|
317
|
+
policy.init(blockSizeSamples = 512)
|
|
318
|
+
policy.onSpeechStateChanged = { speaking ->
|
|
319
|
+
isSpeaking = speaking
|
|
320
|
+
Bridge.sendVoiceActivityDetectionStatus(speaking)
|
|
321
|
+
}
|
|
322
|
+
vadPolicy = policy
|
|
323
|
+
Bridge.log("VadGateSpeechPolicy initialized")
|
|
324
|
+
} catch (e: Exception) {
|
|
325
|
+
Bridge.log("Failed to initialize VadGateSpeechPolicy: ${e.message}")
|
|
326
|
+
vadPolicy = null
|
|
327
|
+
} catch (e: LinkageError) {
|
|
328
|
+
Bridge.log("Failed to initialize VadGateSpeechPolicy: ${e.message}")
|
|
329
|
+
vadPolicy = null
|
|
330
|
+
}
|
|
331
|
+
|
|
289
332
|
// Mic reinit check every 10 seconds
|
|
290
333
|
val micReinitR =
|
|
291
334
|
object : Runnable {
|
|
@@ -590,7 +633,12 @@ class DeviceManager {
|
|
|
590
633
|
var layoutType: String,
|
|
591
634
|
var text: String,
|
|
592
635
|
var data: String?,
|
|
593
|
-
var animationData: Map<String, Any
|
|
636
|
+
var animationData: Map<String, Any>?,
|
|
637
|
+
// Optional bitmap_view container position/size (used by G2; ignored by others)
|
|
638
|
+
var bmpX: Int? = null,
|
|
639
|
+
var bmpY: Int? = null,
|
|
640
|
+
var bmpWidth: Int? = null,
|
|
641
|
+
var bmpHeight: Int? = null
|
|
594
642
|
)
|
|
595
643
|
// MARK: - End Unique
|
|
596
644
|
|
|
@@ -652,9 +700,17 @@ class DeviceManager {
|
|
|
652
700
|
}
|
|
653
701
|
|
|
654
702
|
fun handlePcm(pcmData: ByteArray) {
|
|
703
|
+
// Audio always flows. The previous phone-side Silero VAD gate was a
|
|
704
|
+
// bandwidth-saver that ate transcripts when the mic delivered frames
|
|
705
|
+
// not aligned to 512 samples (the case for Android AudioRecord on the
|
|
706
|
+
// phone internal mic) and was never wired up correctly anyway —
|
|
707
|
+
// `bypass_vad_for_debugging` was dead, cloud-side `bypass_vad` was
|
|
708
|
+
// the only knob, and the policy double-VAD'd what the cloud already
|
|
709
|
+
// VADs server-side. VadGateSpeechPolicy is kept around because
|
|
710
|
+
// hardware-side VAD events from the glasses route through the same
|
|
711
|
+
// class to fire `vad_status` (a separate signal the cloud SDK
|
|
712
|
+
// surfaces as `session.audio.isSpeaking`).
|
|
655
713
|
handleSendingPcm(pcmData)
|
|
656
|
-
|
|
657
|
-
// Send PCM to local transcriber (always needs raw PCM)
|
|
658
714
|
if (shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive) {
|
|
659
715
|
transcriber?.acceptAudio(pcmData)
|
|
660
716
|
}
|
|
@@ -831,7 +887,15 @@ class DeviceManager {
|
|
|
831
887
|
sgc?.sendTextWall("${currentViewState.title}\n\n${currentViewState.text}")
|
|
832
888
|
}
|
|
833
889
|
"bitmap_view" -> {
|
|
834
|
-
currentViewState.data?.let { data ->
|
|
890
|
+
currentViewState.data?.let { data ->
|
|
891
|
+
sgc?.displayBitmap(
|
|
892
|
+
data,
|
|
893
|
+
currentViewState.bmpX,
|
|
894
|
+
currentViewState.bmpY,
|
|
895
|
+
currentViewState.bmpWidth,
|
|
896
|
+
currentViewState.bmpHeight
|
|
897
|
+
)
|
|
898
|
+
}
|
|
835
899
|
}
|
|
836
900
|
"clear_view" -> sgc?.clearDisplay()
|
|
837
901
|
else -> Bridge.log("MAN: UNHANDLED LAYOUT_TYPE ${currentViewState.layoutType}")
|
|
@@ -974,6 +1038,7 @@ class DeviceManager {
|
|
|
974
1038
|
Bridge.log("MAN: Cleaning up previous sgc type: ${sgc?.type}")
|
|
975
1039
|
sgc?.cleanup()
|
|
976
1040
|
sgc = null
|
|
1041
|
+
lastSystemTimeSyncConnectionKey = ""
|
|
977
1042
|
}
|
|
978
1043
|
|
|
979
1044
|
if (sgc != null) {
|
|
@@ -992,10 +1057,9 @@ class DeviceManager {
|
|
|
992
1057
|
} else if (wearable.contains(DeviceTypes.NEX)) {
|
|
993
1058
|
sgc = MentraNex()
|
|
994
1059
|
} else if (wearable.contains(DeviceTypes.MACH1)) {
|
|
995
|
-
sgc =
|
|
1060
|
+
sgc = createOptionalMach1Sgc(DeviceTypes.MACH1)
|
|
996
1061
|
} else if (wearable.contains(DeviceTypes.Z100)) {
|
|
997
|
-
sgc =
|
|
998
|
-
sgc?.type = DeviceTypes.Z100 // Override type to Z100
|
|
1062
|
+
sgc = createOptionalMach1Sgc(DeviceTypes.Z100)
|
|
999
1063
|
} else if (wearable.contains(DeviceTypes.FRAME)) {
|
|
1000
1064
|
// sgc = FrameManager()
|
|
1001
1065
|
}
|
|
@@ -1003,6 +1067,19 @@ class DeviceManager {
|
|
|
1003
1067
|
DeviceStore.apply("glasses", "deviceModel", sgc?.type ?: "")
|
|
1004
1068
|
}
|
|
1005
1069
|
|
|
1070
|
+
private fun createOptionalMach1Sgc(deviceType: String): SGCManager? {
|
|
1071
|
+
return try {
|
|
1072
|
+
Mach1().also { sgc ->
|
|
1073
|
+
if (deviceType == DeviceTypes.Z100) {
|
|
1074
|
+
sgc.type = DeviceTypes.Z100
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
} catch (e: LinkageError) {
|
|
1078
|
+
Bridge.log("Failed to initialize $deviceType support: ${e.message}")
|
|
1079
|
+
null
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1006
1083
|
fun initController(controllerType: String) {
|
|
1007
1084
|
Bridge.log("MAN: Initializing controller: $controllerType")
|
|
1008
1085
|
if (controller != null && controller?.type != controllerType) {
|
|
@@ -1048,8 +1125,24 @@ class DeviceManager {
|
|
|
1048
1125
|
defaultWearable = sgc?.type ?: ""
|
|
1049
1126
|
searching = false
|
|
1050
1127
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1128
|
+
syncSystemTimeOnceForConnection(readyKey)
|
|
1129
|
+
|
|
1130
|
+
// re-apply display height/depth after reconnection
|
|
1131
|
+
mainHandler.postDelayed(
|
|
1132
|
+
{
|
|
1133
|
+
val h =
|
|
1134
|
+
(DeviceStore.store.get("bluetooth", "dashboard_height") as? Number)
|
|
1135
|
+
?.toInt()
|
|
1136
|
+
?: 4
|
|
1137
|
+
val rawDepth =
|
|
1138
|
+
(DeviceStore.store.get("bluetooth", "dashboard_depth") as? Number)
|
|
1139
|
+
?.toInt()
|
|
1140
|
+
?: dashboardDepth // canonical default (2), not 1
|
|
1141
|
+
val d = rawDepth.coerceIn(1, 4)
|
|
1142
|
+
sgc?.setDashboardPosition(h, d)
|
|
1143
|
+
},
|
|
1144
|
+
2000
|
|
1145
|
+
)
|
|
1053
1146
|
|
|
1054
1147
|
// Show welcome message on first connect for all display glasses
|
|
1055
1148
|
if (shouldSendBootingMessage) {
|
|
@@ -1084,6 +1177,21 @@ class DeviceManager {
|
|
|
1084
1177
|
Bridge.saveSetting("device_address", deviceAddress)
|
|
1085
1178
|
}
|
|
1086
1179
|
|
|
1180
|
+
private fun syncSystemTimeOnceForConnection(connectionKey: String) {
|
|
1181
|
+
val activeSgc = sgc ?: return
|
|
1182
|
+
if (activeSgc.type.contains(DeviceTypes.SIMULATED)) {
|
|
1183
|
+
return
|
|
1184
|
+
}
|
|
1185
|
+
if (connectionKey == lastSystemTimeSyncConnectionKey) {
|
|
1186
|
+
return
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
lastSystemTimeSyncConnectionKey = connectionKey
|
|
1190
|
+
val timestampMs = System.currentTimeMillis()
|
|
1191
|
+
Bridge.log("MAN: Syncing glasses system time once for connection: $timestampMs")
|
|
1192
|
+
activeSgc.sendSetSystemTime(timestampMs)
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1087
1195
|
private fun handleG1Ready() {
|
|
1088
1196
|
// G1-specific setup (if any needed in the future)
|
|
1089
1197
|
// Note: G1-specific settings like silent mode, battery status,
|
|
@@ -1096,8 +1204,9 @@ class DeviceManager {
|
|
|
1096
1204
|
|
|
1097
1205
|
fun handleDeviceDisconnected() {
|
|
1098
1206
|
Bridge.log("MAN: Device disconnected")
|
|
1207
|
+
lastSystemTimeSyncConnectionKey = ""
|
|
1099
1208
|
DeviceStore.apply("glasses", "headUp", false)
|
|
1100
|
-
DeviceStore.apply("glasses", "voiceActivityDetectionEnabled",
|
|
1209
|
+
DeviceStore.apply("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
1101
1210
|
}
|
|
1102
1211
|
|
|
1103
1212
|
fun handleControllerReady() {
|
|
@@ -1154,7 +1263,26 @@ class DeviceManager {
|
|
|
1154
1263
|
val title = parsePlaceholders(layout.getString("title", " "))
|
|
1155
1264
|
val data = layout["data"] as? String
|
|
1156
1265
|
|
|
1157
|
-
|
|
1266
|
+
// Optional bitmap_view container position/size (forwarded to the SGC; used by G2).
|
|
1267
|
+
val bmpX = (layout["x"] as? Number)?.toInt()
|
|
1268
|
+
val bmpY = (layout["y"] as? Number)?.toInt()
|
|
1269
|
+
val bmpWidth = (layout["width"] as? Number)?.toInt()
|
|
1270
|
+
val bmpHeight = (layout["height"] as? Number)?.toInt()
|
|
1271
|
+
|
|
1272
|
+
var newViewState =
|
|
1273
|
+
ViewState(
|
|
1274
|
+
topText,
|
|
1275
|
+
bottomText,
|
|
1276
|
+
title,
|
|
1277
|
+
layoutType ?: "",
|
|
1278
|
+
text,
|
|
1279
|
+
data,
|
|
1280
|
+
null,
|
|
1281
|
+
bmpX,
|
|
1282
|
+
bmpY,
|
|
1283
|
+
bmpWidth,
|
|
1284
|
+
bmpHeight
|
|
1285
|
+
)
|
|
1158
1286
|
|
|
1159
1287
|
val currentState = viewStates[stateIndex]
|
|
1160
1288
|
|
|
@@ -1176,6 +1304,12 @@ class DeviceManager {
|
|
|
1176
1304
|
sgc?.showDashboard()
|
|
1177
1305
|
}
|
|
1178
1306
|
|
|
1307
|
+
fun showNotificationsPanel() {
|
|
1308
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
1309
|
+
sgc?.showNotificationsPanel()
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1179
1313
|
fun ping() {
|
|
1180
1314
|
sgc?.ping()
|
|
1181
1315
|
}
|
|
@@ -1232,6 +1366,11 @@ class DeviceManager {
|
|
|
1232
1366
|
sgc?.sendHotspotState(enabled)
|
|
1233
1367
|
}
|
|
1234
1368
|
|
|
1369
|
+
fun setSystemTime(timestampMs: Long) {
|
|
1370
|
+
Bridge.log("MAN: Setting glasses system time: $timestampMs")
|
|
1371
|
+
sgc?.sendSetSystemTime(timestampMs)
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1235
1374
|
fun queryGalleryStatus() {
|
|
1236
1375
|
Bridge.log("MAN: Querying gallery status from glasses")
|
|
1237
1376
|
sgc?.queryGalleryStatus()
|
|
@@ -1251,6 +1390,41 @@ class DeviceManager {
|
|
|
1251
1390
|
(sgc as? MentraLive)?.sendOtaQueryStatus()
|
|
1252
1391
|
}
|
|
1253
1392
|
|
|
1393
|
+
fun sendGalleryMode(requestId: String, enabled: Boolean) {
|
|
1394
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1395
|
+
live.sendGalleryMode(requestId, enabled)
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
fun sendButtonPhotoSettings(requestId: String, size: String) {
|
|
1399
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1400
|
+
live.sendButtonPhotoSettings(requestId, size)
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
fun sendButtonVideoRecordingSettings(requestId: String, width: Int, height: Int, fps: Int) {
|
|
1404
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1405
|
+
live.sendButtonVideoRecordingSettings(requestId, width, height, fps)
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
fun sendButtonCameraLedSetting(requestId: String, enabled: Boolean) {
|
|
1409
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1410
|
+
live.sendButtonCameraLedSetting(requestId, enabled)
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
fun sendButtonMaxRecordingTime(requestId: String, minutes: Int) {
|
|
1414
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1415
|
+
live.sendButtonMaxRecordingTime(requestId, minutes)
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
fun sendCameraFovSetting(requestId: String, fov: Int, roiPosition: Int) {
|
|
1419
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1420
|
+
live.sendCameraFovSetting(requestId, fov, roiPosition)
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
fun retryOtaVersionCheck() {
|
|
1424
|
+
Bridge.log("MAN: ⏰ Retrying glasses OTA version check after clock sync")
|
|
1425
|
+
(sgc as? MentraLive)?.sendOtaRetryVersionCheck()
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1254
1428
|
/**
|
|
1255
1429
|
* Read glasses media step volume (0–15) via K900 on Mentra Live only. Blocks until response,
|
|
1256
1430
|
* error, or timeout (used from JS AsyncFunction on a worker thread).
|
|
@@ -1324,11 +1498,19 @@ class DeviceManager {
|
|
|
1324
1498
|
sgc?.sendReboot()
|
|
1325
1499
|
}
|
|
1326
1500
|
|
|
1327
|
-
fun startVideoRecording(
|
|
1501
|
+
fun startVideoRecording(
|
|
1502
|
+
requestId: String,
|
|
1503
|
+
save: Boolean,
|
|
1504
|
+
sound: Boolean,
|
|
1505
|
+
width: Int = 0,
|
|
1506
|
+
height: Int = 0,
|
|
1507
|
+
fps: Int = 0,
|
|
1508
|
+
) {
|
|
1328
1509
|
Bridge.log(
|
|
1329
|
-
"MAN: onStartVideoRecording: requestId=$requestId, save=$save, flash=true, sound=$sound"
|
|
1510
|
+
"MAN: onStartVideoRecording: requestId=$requestId, save=$save, flash=true, sound=$sound, " +
|
|
1511
|
+
"resolution=${width}x${height}@${fps}fps"
|
|
1330
1512
|
)
|
|
1331
|
-
sgc?.startVideoRecording(requestId, save, true, sound)
|
|
1513
|
+
sgc?.startVideoRecording(requestId, save, true, sound, width, height, fps)
|
|
1332
1514
|
}
|
|
1333
1515
|
|
|
1334
1516
|
fun stopVideoRecording(requestId: String) {
|
|
@@ -1339,7 +1521,15 @@ class DeviceManager {
|
|
|
1339
1521
|
fun setMicState() {
|
|
1340
1522
|
val willSendPcm = shouldSendPcm || shouldSendLc3
|
|
1341
1523
|
val willSendTranscript = shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive
|
|
1342
|
-
|
|
1524
|
+
val nextEnabled = willSendPcm || willSendTranscript
|
|
1525
|
+
// Tell VAD when the mic is shutting down so it doesn't get stuck in
|
|
1526
|
+
// a stale "speaking" state and keep emitting vad_status=true after
|
|
1527
|
+
// audio stops flowing.
|
|
1528
|
+
if (micEnabled && !nextEnabled) {
|
|
1529
|
+
vadPolicy?.microphoneStateChanged(false)
|
|
1530
|
+
}
|
|
1531
|
+
micEnabled = nextEnabled
|
|
1532
|
+
vadBuffer.clear()
|
|
1343
1533
|
updateMicState()
|
|
1344
1534
|
}
|
|
1345
1535
|
|
|
@@ -1351,6 +1541,7 @@ class DeviceManager {
|
|
|
1351
1541
|
authToken: String?,
|
|
1352
1542
|
compress: String,
|
|
1353
1543
|
flash: Boolean,
|
|
1544
|
+
save: Boolean,
|
|
1354
1545
|
sound: Boolean,
|
|
1355
1546
|
exposureTimeNs: Double? = null,
|
|
1356
1547
|
iso: Int? = null,
|
|
@@ -1364,7 +1555,7 @@ class DeviceManager {
|
|
|
1364
1555
|
}
|
|
1365
1556
|
val manualIso = if (exposureNs != null) iso?.takeIf { it > 0 } else null
|
|
1366
1557
|
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"}"
|
|
1558
|
+
"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
1559
|
)
|
|
1369
1560
|
val activeSgc = sgc
|
|
1370
1561
|
if (activeSgc == null) {
|
|
@@ -1373,7 +1564,7 @@ class DeviceManager {
|
|
|
1373
1564
|
)
|
|
1374
1565
|
return
|
|
1375
1566
|
}
|
|
1376
|
-
activeSgc.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, sound, exposureNs, manualIso)
|
|
1567
|
+
activeSgc.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, save, sound, exposureNs, manualIso)
|
|
1377
1568
|
}
|
|
1378
1569
|
|
|
1379
1570
|
fun rgbLedControl(
|
|
@@ -1495,6 +1686,7 @@ class DeviceManager {
|
|
|
1495
1686
|
sgc?.clearDisplay()
|
|
1496
1687
|
sgc?.disconnect()
|
|
1497
1688
|
sgc = null // Clear the SGC reference after disconnect
|
|
1689
|
+
lastSystemTimeSyncConnectionKey = ""
|
|
1498
1690
|
searching = false
|
|
1499
1691
|
micEnabled = false
|
|
1500
1692
|
updateMicState()
|
|
@@ -1503,7 +1695,7 @@ class DeviceManager {
|
|
|
1503
1695
|
DeviceStore.apply("glasses", "deviceModel", "")
|
|
1504
1696
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
1505
1697
|
DeviceStore.apply("glasses", "connected", false)
|
|
1506
|
-
DeviceStore.apply("glasses", "voiceActivityDetectionEnabled",
|
|
1698
|
+
DeviceStore.apply("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
1507
1699
|
// disconnect the controller as well:
|
|
1508
1700
|
searchingController = false
|
|
1509
1701
|
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)
|
|
@@ -121,9 +121,10 @@ object DeviceStore {
|
|
|
121
121
|
store.set("bluetooth", "dashboard_height", 4)
|
|
122
122
|
store.set("bluetooth", "dashboard_depth", 2)
|
|
123
123
|
store.set("bluetooth", "head_up_angle", 30)
|
|
124
|
+
store.set("bluetooth", "imu_enabled", false)
|
|
124
125
|
store.set("bluetooth", "contextual_dashboard", true)
|
|
125
126
|
store.set("bluetooth", "gallery_mode", true)
|
|
126
|
-
store.set("bluetooth", "voice_activity_detection_enabled",
|
|
127
|
+
store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
127
128
|
store.set("bluetooth", "screen_disabled", false)
|
|
128
129
|
store.set("bluetooth", "button_photo_size", "medium")
|
|
129
130
|
store.set("bluetooth", "button_camera_led", true)
|
|
@@ -140,6 +141,9 @@ object DeviceStore {
|
|
|
140
141
|
store.set("bluetooth", "should_send_lc3", false)
|
|
141
142
|
store.set("bluetooth", "should_send_transcript", false)
|
|
142
143
|
store.set("bluetooth", "use_native_dashboard", false)
|
|
144
|
+
// Mentra Nex feature flags (off by default; toggled from Nex Developer Settings):
|
|
145
|
+
store.set("bluetooth", "nex_chinese_captions", false)
|
|
146
|
+
store.set("bluetooth", "nex_audio_playback", false)
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
fun get(category: String, key: String): Any? {
|
|
@@ -235,18 +239,42 @@ object DeviceStore {
|
|
|
235
239
|
DeviceManager.getInstance().sgc?.setHeadUpAngle(angle)
|
|
236
240
|
}
|
|
237
241
|
}
|
|
242
|
+
"bluetooth" to "imu_enabled" -> {
|
|
243
|
+
(value as? Boolean)?.let { enabled ->
|
|
244
|
+
CoroutineScope(Dispatchers.Main).launch {
|
|
245
|
+
DeviceManager.getInstance().sgc?.setImuEnabled(enabled)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
238
249
|
"bluetooth" to "menu_apps" -> {
|
|
239
250
|
@Suppress("UNCHECKED_CAST")
|
|
240
251
|
(value as? List<Map<String, Any>>)?.let { items ->
|
|
241
252
|
DeviceManager.getInstance().sgc?.setDashboardMenu(items)
|
|
242
253
|
}
|
|
243
254
|
}
|
|
255
|
+
// `core` category is normalized to `bluetooth` before reaching this switch.
|
|
256
|
+
"bluetooth" to "calendar_events" -> {
|
|
257
|
+
@Suppress("UNCHECKED_CAST")
|
|
258
|
+
(value as? List<Map<String, Any>>)?.let { events ->
|
|
259
|
+
DeviceManager.getInstance().sgc?.sendCalendarEvents(events)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
"bluetooth" to "metric_system",
|
|
263
|
+
"bluetooth" to "twelve_hour_time" -> {
|
|
264
|
+
DeviceManager.getInstance().sgc?.sendDashboardDisplaySettings()
|
|
265
|
+
}
|
|
244
266
|
"bluetooth" to "gallery_mode" -> {
|
|
245
267
|
DeviceManager.getInstance().sgc?.sendGalleryMode()
|
|
246
268
|
}
|
|
247
269
|
"bluetooth" to "voice_activity_detection_enabled" -> {
|
|
248
270
|
DeviceManager.getInstance().sgc?.sendVoiceActivityDetectionSetting()
|
|
249
271
|
}
|
|
272
|
+
"bluetooth" to "nex_audio_playback" -> {
|
|
273
|
+
(value as? Boolean)?.let { enabled ->
|
|
274
|
+
Bridge.log("DeviceStore: nex_audio_playback changed to $enabled")
|
|
275
|
+
DeviceManager.getInstance().sgc?.applyNexAudioPlaybackSetting()
|
|
276
|
+
}
|
|
277
|
+
}
|
|
250
278
|
"bluetooth" to "screen_disabled" -> {
|
|
251
279
|
(value as? Boolean)?.let { disabled ->
|
|
252
280
|
if (disabled) {
|