@mentra/bluetooth-sdk 0.1.14 → 0.1.17
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 +4 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +22 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +358 -261
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +133 -52
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +12 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +22 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +50 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -795
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +422 -78
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +3 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +5 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +60 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
- package/build/BluetoothSdk.types.d.ts +39 -14
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +4 -4
- package/build/_internal.js +4 -4
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +11 -3
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +5 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/cameraRequestPayload.d.ts +4 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -0
- package/build/_private/cameraRequestPayload.js +25 -0
- package/build/_private/cameraRequestPayload.js.map +1 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +8 -2
- 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 +72 -54
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +40 -12
- package/ios/Source/BluetoothSdkDefaults.swift +2 -30
- package/ios/Source/Bridge.swift +4 -16
- package/ios/Source/DeviceManager.swift +72 -12
- package/ios/Source/MentraBluetoothSDK.swift +120 -50
- package/ios/Source/OtaManifest.swift +13 -13
- package/ios/Source/camera/CameraModels.swift +75 -9
- package/ios/Source/errors/{BluetoothError.swift → BluetoothSdkError.swift} +1 -1
- package/ios/Source/events/BluetoothEvents.swift +5 -27
- package/ios/Source/internal/BluetoothAvailability.swift +5 -5
- package/ios/Source/sgcs/G1.swift +51 -1
- package/ios/Source/sgcs/G2.swift +643 -203
- package/ios/Source/sgcs/MentraLive.swift +53 -39
- package/ios/Source/sgcs/Nimo.swift +1928 -0
- package/ios/Source/sgcs/SGCManager.swift +12 -5
- package/ios/Source/stt/STTTools.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +5 -0
- package/ios/Source/utils/Constants.swift +2 -0
- package/package.json +9 -1
- package/scripts/inject-ios-sdk-version.mjs +65 -0
- package/src/BluetoothSdk.types.ts +42 -17
- package/src/_internal.ts +4 -4
- package/src/_private/BluetoothSdkModule.ts +18 -2
- package/src/_private/cameraRequestPayload.ts +29 -0
- package/src/_private/photoRequestPayload.ts +9 -2
- package/src/index.ts +76 -58
|
@@ -19,6 +19,7 @@ import com.mentra.bluetoothsdk.sgcs.G2
|
|
|
19
19
|
import com.mentra.bluetoothsdk.sgcs.Mach1
|
|
20
20
|
import com.mentra.bluetoothsdk.sgcs.MentraLive
|
|
21
21
|
import com.mentra.bluetoothsdk.sgcs.MentraNex
|
|
22
|
+
import com.mentra.bluetoothsdk.sgcs.Nimo
|
|
22
23
|
import com.mentra.bluetoothsdk.sgcs.SGCManager
|
|
23
24
|
import com.mentra.bluetoothsdk.sgcs.Simulated
|
|
24
25
|
import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
@@ -41,12 +42,13 @@ import kotlin.jvm.JvmStatic
|
|
|
41
42
|
class DeviceManager {
|
|
42
43
|
companion object {
|
|
43
44
|
|
|
44
|
-
@Volatile
|
|
45
|
+
@Volatile
|
|
46
|
+
private var _instance: DeviceManager? = null
|
|
45
47
|
|
|
46
48
|
@JvmStatic
|
|
47
49
|
fun getInstance(): DeviceManager {
|
|
48
50
|
return _instance
|
|
49
|
-
|
|
51
|
+
?: synchronized(this) { _instance ?: DeviceManager().also { _instance = it } }
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -189,10 +191,10 @@ class DeviceManager {
|
|
|
189
191
|
|
|
190
192
|
public var micRanking: MutableList<String>
|
|
191
193
|
get() =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
(DeviceStore.store.get("bluetooth", "micRanking") as? List<*>)
|
|
195
|
+
?.mapNotNull { it as? String }
|
|
196
|
+
?.toMutableList()
|
|
197
|
+
?: MicMap.map["auto"]?.toMutableList() ?: mutableListOf()
|
|
196
198
|
set(value) = DeviceStore.apply("bluetooth", "micRanking", value)
|
|
197
199
|
|
|
198
200
|
private var shouldSendBootingMessage: Boolean
|
|
@@ -238,11 +240,13 @@ class DeviceManager {
|
|
|
238
240
|
LC3,
|
|
239
241
|
PCM
|
|
240
242
|
}
|
|
243
|
+
|
|
241
244
|
// Canonical LC3 config: 16kHz sample rate, 10ms frame duration
|
|
242
245
|
// Frame size is configurable: 20 bytes (16kbps), 40 bytes (32kbps), 60 bytes (48kbps)
|
|
243
246
|
private var lc3EncoderPtr: Long = 0
|
|
244
247
|
private var lc3DecoderPtr: Long = 0
|
|
245
248
|
private val lc3Lock = Any()
|
|
249
|
+
|
|
246
250
|
// Audio output format - defaults to LC3 for bandwidth savings
|
|
247
251
|
private var audioOutputFormat: AudioOutputFormat = AudioOutputFormat.LC3
|
|
248
252
|
private var lastLc3Event: Long? = null
|
|
@@ -267,32 +271,9 @@ class DeviceManager {
|
|
|
267
271
|
// setupPermissionMonitoring()
|
|
268
272
|
setupBluetoothStateMonitoring()
|
|
269
273
|
phoneMic = PhoneMic.getInstance()
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
transcriber = SherpaOnnxTranscriber(context)
|
|
274
|
-
transcriber?.setTranscriptListener(
|
|
275
|
-
object : SherpaOnnxTranscriber.TranscriptListener {
|
|
276
|
-
override fun onPartialResult(text: String, language: String) {
|
|
277
|
-
Bridge.log("STT: Partial result: $text")
|
|
278
|
-
Bridge.sendLocalTranscription(text, false, language)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
override fun onFinalResult(text: String, language: String) {
|
|
282
|
-
Bridge.log("STT: Final result: $text")
|
|
283
|
-
Bridge.sendLocalTranscription(text, true, language)
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
)
|
|
287
|
-
transcriber?.initialize()
|
|
288
|
-
Bridge.log("SherpaOnnxTranscriber fully initialized")
|
|
289
|
-
} catch (e: Exception) {
|
|
290
|
-
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
291
|
-
transcriber = null
|
|
292
|
-
} catch (e: LinkageError) {
|
|
293
|
-
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
294
|
-
transcriber = null
|
|
295
|
-
}
|
|
274
|
+
// Sherpa is intentionally lazy. Initializing it during app startup
|
|
275
|
+
// competes with Cloud V2 captions even when cloud transcription is
|
|
276
|
+
// healthy; start it only when offline/local transcription is requested.
|
|
296
277
|
|
|
297
278
|
// Initialize LC3 encoder/decoder for unified audio encoding
|
|
298
279
|
try {
|
|
@@ -332,12 +313,12 @@ class DeviceManager {
|
|
|
332
313
|
|
|
333
314
|
// Mic reinit check every 10 seconds
|
|
334
315
|
val micReinitR =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
316
|
+
object : Runnable {
|
|
317
|
+
override fun run() {
|
|
318
|
+
checkAndReinitGlassesMic()
|
|
319
|
+
mainHandler.postDelayed(this, 10_000)
|
|
340
320
|
}
|
|
321
|
+
}
|
|
341
322
|
micReinitRunnable = micReinitR
|
|
342
323
|
mainHandler.postDelayed(micReinitR, 10_000)
|
|
343
324
|
}
|
|
@@ -365,26 +346,26 @@ class DeviceManager {
|
|
|
365
346
|
}
|
|
366
347
|
|
|
367
348
|
val recheck =
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
349
|
+
object : Runnable {
|
|
350
|
+
override fun run() {
|
|
351
|
+
systemMicAvailabilityRecheckRunnable = null
|
|
371
352
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
val stillBlocked = phoneMic?.hasBlockingMicInterruption() ?: true
|
|
377
|
-
if (stillBlocked) {
|
|
378
|
-
scheduleSystemMicAvailabilityRecheck(reason)
|
|
379
|
-
return
|
|
380
|
-
}
|
|
353
|
+
if (!micEnabled || !systemMicUnavailable) {
|
|
354
|
+
return
|
|
355
|
+
}
|
|
381
356
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
357
|
+
val stillBlocked = phoneMic?.hasBlockingMicInterruption() ?: true
|
|
358
|
+
if (stillBlocked) {
|
|
359
|
+
scheduleSystemMicAvailabilityRecheck(reason)
|
|
360
|
+
return
|
|
386
361
|
}
|
|
362
|
+
|
|
363
|
+
systemMicUnavailable = false
|
|
364
|
+
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE recheck_after_$reason")
|
|
365
|
+
appendLog("MAN: MIC_UNAVAILABLE: FALSE recheck_after_$reason")
|
|
366
|
+
updateMicState()
|
|
387
367
|
}
|
|
368
|
+
}
|
|
388
369
|
|
|
389
370
|
systemMicAvailabilityRecheckRunnable = recheck
|
|
390
371
|
mainHandler.postDelayed(recheck, 2_000)
|
|
@@ -399,30 +380,30 @@ class DeviceManager {
|
|
|
399
380
|
lastHadMicrophonePermission = checkMicrophonePermission(context)
|
|
400
381
|
|
|
401
382
|
Bridge.log(
|
|
402
|
-
|
|
383
|
+
"MAN: Initial permissions - BT: $lastHadBluetoothPermission, Mic: $lastHadMicrophonePermission"
|
|
403
384
|
)
|
|
404
385
|
|
|
405
386
|
// Create receiver for package changes (fires when permissions change)
|
|
406
387
|
permissionReceiver =
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
388
|
+
object : BroadcastReceiver() {
|
|
389
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
390
|
+
if (intent?.action == Intent.ACTION_PACKAGE_CHANGED &&
|
|
391
|
+
intent.data?.schemeSpecificPart == context?.packageName
|
|
392
|
+
) {
|
|
393
|
+
|
|
394
|
+
Bridge.log("MAN: Package changed, checking permissions...")
|
|
395
|
+
checkPermissionChanges()
|
|
416
396
|
}
|
|
417
397
|
}
|
|
398
|
+
}
|
|
418
399
|
|
|
419
400
|
// Register the receiver
|
|
420
401
|
try {
|
|
421
402
|
val filter =
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
403
|
+
IntentFilter().apply {
|
|
404
|
+
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
|
405
|
+
addDataScheme("package")
|
|
406
|
+
}
|
|
426
407
|
context.registerReceiver(permissionReceiver, filter)
|
|
427
408
|
Bridge.log("MAN: Permission monitoring started")
|
|
428
409
|
} catch (e: Exception) {
|
|
@@ -435,12 +416,12 @@ class DeviceManager {
|
|
|
435
416
|
|
|
436
417
|
private fun startPeriodicPermissionCheck() {
|
|
437
418
|
permissionCheckRunnable =
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
419
|
+
object : Runnable {
|
|
420
|
+
override fun run() {
|
|
421
|
+
checkPermissionChanges()
|
|
422
|
+
handler.postDelayed(this, 10000) // Check every 10 seconds
|
|
443
423
|
}
|
|
424
|
+
}
|
|
444
425
|
handler.postDelayed(permissionCheckRunnable!!, 10000)
|
|
445
426
|
}
|
|
446
427
|
|
|
@@ -454,7 +435,7 @@ class DeviceManager {
|
|
|
454
435
|
|
|
455
436
|
if (currentHasBluetoothPermission != lastHadBluetoothPermission) {
|
|
456
437
|
Bridge.log(
|
|
457
|
-
|
|
438
|
+
"MAN: Bluetooth permission changed: $lastHadBluetoothPermission -> $currentHasBluetoothPermission"
|
|
458
439
|
)
|
|
459
440
|
lastHadBluetoothPermission = currentHasBluetoothPermission
|
|
460
441
|
permissionsChanged = true
|
|
@@ -462,7 +443,7 @@ class DeviceManager {
|
|
|
462
443
|
|
|
463
444
|
if (currentHasMicrophonePermission != lastHadMicrophonePermission) {
|
|
464
445
|
Bridge.log(
|
|
465
|
-
|
|
446
|
+
"MAN: Microphone permission changed: $lastHadMicrophonePermission -> $currentHasMicrophonePermission"
|
|
466
447
|
)
|
|
467
448
|
lastHadMicrophonePermission = currentHasMicrophonePermission
|
|
468
449
|
permissionsChanged = true
|
|
@@ -482,17 +463,17 @@ class DeviceManager {
|
|
|
482
463
|
private fun checkBluetoothPermission(context: Context): Boolean {
|
|
483
464
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
484
465
|
val connect = ContextCompat.checkSelfPermission(
|
|
485
|
-
|
|
486
|
-
|
|
466
|
+
context,
|
|
467
|
+
android.Manifest.permission.BLUETOOTH_CONNECT
|
|
487
468
|
) == PackageManager.PERMISSION_GRANTED
|
|
488
469
|
val scan = ContextCompat.checkSelfPermission(
|
|
489
|
-
|
|
490
|
-
|
|
470
|
+
context,
|
|
471
|
+
android.Manifest.permission.BLUETOOTH_SCAN
|
|
491
472
|
) == PackageManager.PERMISSION_GRANTED
|
|
492
473
|
connect && scan
|
|
493
474
|
} else {
|
|
494
475
|
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH) ==
|
|
495
|
-
|
|
476
|
+
PackageManager.PERMISSION_GRANTED
|
|
496
477
|
}
|
|
497
478
|
}
|
|
498
479
|
|
|
@@ -502,8 +483,8 @@ class DeviceManager {
|
|
|
502
483
|
|
|
503
484
|
private fun checkMicrophonePermission(context: Context): Boolean {
|
|
504
485
|
return ContextCompat.checkSelfPermission(
|
|
505
|
-
|
|
506
|
-
|
|
486
|
+
context,
|
|
487
|
+
android.Manifest.permission.RECORD_AUDIO
|
|
507
488
|
) == PackageManager.PERMISSION_GRANTED
|
|
508
489
|
}
|
|
509
490
|
|
|
@@ -511,42 +492,45 @@ class DeviceManager {
|
|
|
511
492
|
val context = Bridge.getContext()
|
|
512
493
|
|
|
513
494
|
bluetoothStateReceiver =
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
495
|
+
object : BroadcastReceiver() {
|
|
496
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
497
|
+
if (intent?.action != BluetoothAdapter.ACTION_STATE_CHANGED) return
|
|
498
|
+
|
|
499
|
+
val state =
|
|
500
|
+
intent.getIntExtra(
|
|
501
|
+
BluetoothAdapter.EXTRA_STATE,
|
|
502
|
+
BluetoothAdapter.ERROR
|
|
503
|
+
)
|
|
504
|
+
when (state) {
|
|
505
|
+
BluetoothAdapter.STATE_OFF -> {
|
|
506
|
+
Bridge.log("MAN: Bluetooth turned OFF (control center or settings)")
|
|
507
|
+
disconnect()
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
BluetoothAdapter.STATE_TURNING_OFF -> {
|
|
511
|
+
Bridge.log("MAN: Bluetooth turning off...")
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
BluetoothAdapter.STATE_ON -> {
|
|
515
|
+
Bridge.log("MAN: Bluetooth turned ON")
|
|
516
|
+
// Auto-reconnect to last known device if we have one
|
|
517
|
+
if (defaultWearable.isNotEmpty() && deviceName.isNotEmpty()) {
|
|
518
|
+
Bridge.log(
|
|
519
|
+
"MAN: Bluetooth restored, attempting reconnect to: $deviceName"
|
|
522
520
|
)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
BluetoothAdapter.STATE_TURNING_OFF -> {
|
|
529
|
-
Bridge.log("MAN: Bluetooth turning off...")
|
|
530
|
-
}
|
|
531
|
-
BluetoothAdapter.STATE_ON -> {
|
|
532
|
-
Bridge.log("MAN: Bluetooth turned ON")
|
|
533
|
-
// Auto-reconnect to last known device if we have one
|
|
534
|
-
if (defaultWearable.isNotEmpty() && deviceName.isNotEmpty()) {
|
|
535
|
-
Bridge.log(
|
|
536
|
-
"MAN: Bluetooth restored, attempting reconnect to: $deviceName"
|
|
537
|
-
)
|
|
538
|
-
handler.postDelayed(
|
|
539
|
-
{ connectDefault() },
|
|
540
|
-
2000
|
|
541
|
-
) // Small delay to let BT stack stabilize
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
BluetoothAdapter.STATE_TURNING_ON -> {
|
|
545
|
-
Bridge.log("MAN: Bluetooth turning on...")
|
|
521
|
+
handler.postDelayed(
|
|
522
|
+
{ connectDefault() },
|
|
523
|
+
2000
|
|
524
|
+
) // Small delay to let BT stack stabilize
|
|
546
525
|
}
|
|
547
526
|
}
|
|
527
|
+
|
|
528
|
+
BluetoothAdapter.STATE_TURNING_ON -> {
|
|
529
|
+
Bridge.log("MAN: Bluetooth turning on...")
|
|
530
|
+
}
|
|
548
531
|
}
|
|
549
532
|
}
|
|
533
|
+
}
|
|
550
534
|
|
|
551
535
|
try {
|
|
552
536
|
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
|
@@ -620,35 +604,35 @@ class DeviceManager {
|
|
|
620
604
|
// Matching Swift's 4 view states exactly
|
|
621
605
|
viewStates.add(ViewState(" ", " ", " ", "text_wall", "", null, null))
|
|
622
606
|
viewStates.add(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
607
|
+
ViewState(
|
|
608
|
+
" ",
|
|
609
|
+
" ",
|
|
610
|
+
" ",
|
|
611
|
+
"text_wall",
|
|
612
|
+
"\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
|
|
613
|
+
null,
|
|
614
|
+
null
|
|
615
|
+
)
|
|
632
616
|
)
|
|
633
617
|
viewStates.add(ViewState(" ", " ", " ", "text_wall", "", null, null))
|
|
634
618
|
viewStates.add(
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
619
|
+
ViewState(
|
|
620
|
+
" ",
|
|
621
|
+
" ",
|
|
622
|
+
" ",
|
|
623
|
+
"text_wall",
|
|
624
|
+
"\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
|
|
625
|
+
null,
|
|
626
|
+
null
|
|
627
|
+
)
|
|
644
628
|
)
|
|
645
629
|
}
|
|
646
630
|
|
|
647
631
|
private fun statesEqual(s1: ViewState, s2: ViewState): Boolean {
|
|
648
632
|
val state1 =
|
|
649
|
-
|
|
633
|
+
"${s1.layoutType}${s1.text}${s1.topText}${s1.bottomText}${s1.title}${s1.data ?: ""}"
|
|
650
634
|
val state2 =
|
|
651
|
-
|
|
635
|
+
"${s2.layoutType}${s2.text}${s2.topText}${s2.bottomText}${s2.title}${s2.data ?: ""}"
|
|
652
636
|
return state1 == state2
|
|
653
637
|
}
|
|
654
638
|
|
|
@@ -659,18 +643,22 @@ class DeviceManager {
|
|
|
659
643
|
// Inner classes
|
|
660
644
|
|
|
661
645
|
data class ViewState(
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
646
|
+
var topText: String,
|
|
647
|
+
var bottomText: String,
|
|
648
|
+
var title: String,
|
|
649
|
+
var layoutType: String,
|
|
650
|
+
var text: String,
|
|
651
|
+
var data: String?,
|
|
652
|
+
var animationData: Map<String, Any>?,
|
|
653
|
+
// Optional bitmap_view container position/size (used by G2; ignored by others).
|
|
654
|
+
// Reused by positioned_text for its container rect.
|
|
655
|
+
var bmpX: Int? = null,
|
|
656
|
+
var bmpY: Int? = null,
|
|
657
|
+
var bmpWidth: Int? = null,
|
|
658
|
+
var bmpHeight: Int? = null,
|
|
659
|
+
// Optional positioned_text border (used by G2; ignored by others).
|
|
660
|
+
var borderWidth: Int? = null,
|
|
661
|
+
var borderRadius: Int? = null
|
|
674
662
|
)
|
|
675
663
|
// MARK: - End Unique
|
|
676
664
|
|
|
@@ -701,6 +689,15 @@ class DeviceManager {
|
|
|
701
689
|
}
|
|
702
690
|
}
|
|
703
691
|
|
|
692
|
+
/**
|
|
693
|
+
* Marks glasses-mic audio as alive for the 10s reinit watchdog. SGCs that decode
|
|
694
|
+
* audio themselves and feed [handlePcm] directly (e.g. Nimo's Opus path) must call
|
|
695
|
+
* this per uplink packet — otherwise the watchdog keeps re-enabling a working mic.
|
|
696
|
+
*/
|
|
697
|
+
fun reportGlassesAudioActivity() {
|
|
698
|
+
lastLc3Event = System.currentTimeMillis()
|
|
699
|
+
}
|
|
700
|
+
|
|
704
701
|
/**
|
|
705
702
|
* Handle raw LC3 audio data from glasses. Decodes the glasses LC3, then passes to handlePcm for
|
|
706
703
|
* canonical LC3 encoding. Note: frameSize here is for glasses→phone decoding, NOT for
|
|
@@ -744,7 +741,9 @@ class DeviceManager {
|
|
|
744
741
|
// surfaces as `session.audio.isSpeaking`).
|
|
745
742
|
handleSendingPcm(pcmData)
|
|
746
743
|
if (shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive) {
|
|
747
|
-
|
|
744
|
+
if (ensureTranscriberInitialized()) {
|
|
745
|
+
transcriber?.acceptAudio(pcmData)
|
|
746
|
+
}
|
|
748
747
|
}
|
|
749
748
|
}
|
|
750
749
|
|
|
@@ -785,8 +784,8 @@ class DeviceManager {
|
|
|
785
784
|
|
|
786
785
|
for (micMode in micRanking) {
|
|
787
786
|
if (micMode == MicTypes.PHONE_INTERNAL ||
|
|
788
|
-
|
|
789
|
-
|
|
787
|
+
micMode == MicTypes.BLUETOOTH_CLASSIC ||
|
|
788
|
+
micMode == MicTypes.BLUETOOTH
|
|
790
789
|
) {
|
|
791
790
|
|
|
792
791
|
if (phoneMic?.isRecordingWithMode(micMode) == true) {
|
|
@@ -848,8 +847,8 @@ class DeviceManager {
|
|
|
848
847
|
}
|
|
849
848
|
|
|
850
849
|
if (micMode == MicTypes.PHONE_INTERNAL ||
|
|
851
|
-
|
|
852
|
-
|
|
850
|
+
micMode == MicTypes.BLUETOOTH_CLASSIC ||
|
|
851
|
+
micMode == MicTypes.BLUETOOTH
|
|
853
852
|
) {
|
|
854
853
|
phoneMic?.stopMode(micMode)
|
|
855
854
|
}
|
|
@@ -912,23 +911,39 @@ class DeviceManager {
|
|
|
912
911
|
"text_wall" -> {
|
|
913
912
|
sgc?.sendTextWall(currentViewState.text)
|
|
914
913
|
}
|
|
914
|
+
|
|
915
915
|
"double_text_wall" -> {
|
|
916
916
|
sgc?.sendDoubleTextWall(currentViewState.topText, currentViewState.bottomText)
|
|
917
917
|
}
|
|
918
|
+
|
|
918
919
|
"reference_card" -> {
|
|
919
920
|
sgc?.sendTextWall("${currentViewState.title}\n\n${currentViewState.text}")
|
|
920
921
|
}
|
|
922
|
+
|
|
921
923
|
"bitmap_view" -> {
|
|
922
924
|
currentViewState.data?.let { data ->
|
|
923
925
|
sgc?.displayBitmap(
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
926
|
+
data,
|
|
927
|
+
currentViewState.bmpX,
|
|
928
|
+
currentViewState.bmpY,
|
|
929
|
+
currentViewState.bmpWidth,
|
|
930
|
+
currentViewState.bmpHeight
|
|
929
931
|
)
|
|
930
932
|
}
|
|
931
933
|
}
|
|
934
|
+
|
|
935
|
+
"positioned_text" -> {
|
|
936
|
+
sgc?.sendPositionedText(
|
|
937
|
+
currentViewState.text,
|
|
938
|
+
currentViewState.bmpX ?: 0,
|
|
939
|
+
currentViewState.bmpY ?: 0,
|
|
940
|
+
currentViewState.bmpWidth ?: 576,
|
|
941
|
+
currentViewState.bmpHeight ?: 288,
|
|
942
|
+
currentViewState.borderWidth ?: 0,
|
|
943
|
+
currentViewState.borderRadius ?: 0
|
|
944
|
+
)
|
|
945
|
+
}
|
|
946
|
+
|
|
932
947
|
"clear_view" -> sgc?.clearDisplay()
|
|
933
948
|
else -> Bridge.log("MAN: UNHANDLED LAYOUT_TYPE ${currentViewState.layoutType}")
|
|
934
949
|
}
|
|
@@ -949,15 +964,15 @@ class DeviceManager {
|
|
|
949
964
|
val currentDate = dateFormat.format(Date())
|
|
950
965
|
|
|
951
966
|
val placeholders =
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
967
|
+
mapOf(
|
|
968
|
+
"\$no_datetime$" to formattedDate,
|
|
969
|
+
"\$DATE$" to currentDate,
|
|
970
|
+
"\$TIME12$" to time12,
|
|
971
|
+
"\$TIME24$" to time24,
|
|
972
|
+
"\$GBATT$" to
|
|
973
|
+
(sgc?.batteryLevel?.let { if (it == -1) "" else "$it%" } ?: ""),
|
|
974
|
+
"\$CONNECTION_STATUS$" to "Connected"
|
|
975
|
+
)
|
|
961
976
|
|
|
962
977
|
return placeholders.entries.fold(text) { result, (key, value) ->
|
|
963
978
|
result.replace(key, value)
|
|
@@ -978,83 +993,89 @@ class DeviceManager {
|
|
|
978
993
|
// Another app is using the microphone
|
|
979
994
|
systemMicUnavailable = true
|
|
980
995
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
981
|
-
appendLog("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
982
|
-
scheduleSystemMicAvailabilityRecheck("external_app_recording")
|
|
996
|
+
// appendLog("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
|
|
997
|
+
// scheduleSystemMicAvailabilityRecheck("external_app_recording")
|
|
983
998
|
}
|
|
999
|
+
|
|
984
1000
|
"audio_focus_available" -> {
|
|
985
1001
|
// Audio focus is available again
|
|
986
1002
|
systemMicUnavailable = false
|
|
987
1003
|
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE audio_focus_available")
|
|
988
|
-
appendLog("MAN: MIC_UNAVAILABLE: FALSE audio_focus_available")
|
|
1004
|
+
// appendLog("MAN: MIC_UNAVAILABLE: FALSE audio_focus_available")
|
|
989
1005
|
}
|
|
1006
|
+
|
|
990
1007
|
"external_app_stopped" -> {
|
|
991
1008
|
// External app stopped recording
|
|
992
1009
|
systemMicUnavailable = false
|
|
993
1010
|
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE external_app_stopped")
|
|
994
|
-
appendLog("MAN: MIC_UNAVAILABLE: FALSE external_app_stopped")
|
|
1011
|
+
// appendLog("MAN: MIC_UNAVAILABLE: FALSE external_app_stopped")
|
|
995
1012
|
}
|
|
1013
|
+
|
|
996
1014
|
"phone_call_interruption" -> {
|
|
997
1015
|
// Phone call started - mark mic as unavailable
|
|
998
1016
|
systemMicUnavailable = true
|
|
999
1017
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
|
|
1000
1018
|
appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
|
|
1001
|
-
scheduleSystemMicAvailabilityRecheck("phone_call_interruption")
|
|
1019
|
+
// scheduleSystemMicAvailabilityRecheck("phone_call_interruption")
|
|
1002
1020
|
}
|
|
1021
|
+
|
|
1003
1022
|
"phone_call_ended" -> {
|
|
1004
1023
|
// Phone call ended - mark mic as available again
|
|
1005
1024
|
systemMicUnavailable = false
|
|
1006
1025
|
Bridge.log("MAN: MIC_UNAVAILABLE: FALSE phone_call_ended")
|
|
1007
|
-
appendLog("MAN: MIC_UNAVAILABLE: FALSE phone_call_ended")
|
|
1026
|
+
// appendLog("MAN: MIC_UNAVAILABLE: FALSE phone_call_ended")
|
|
1008
1027
|
}
|
|
1028
|
+
|
|
1009
1029
|
"phone_call_active" -> {
|
|
1010
1030
|
// Tried to start recording while phone call already active
|
|
1011
1031
|
systemMicUnavailable = true
|
|
1012
1032
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
1013
|
-
appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
1014
|
-
scheduleSystemMicAvailabilityRecheck("phone_call_active")
|
|
1033
|
+
// appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
|
|
1015
1034
|
}
|
|
1035
|
+
|
|
1016
1036
|
"audio_focus_denied" -> {
|
|
1017
1037
|
// Another app has audio focus
|
|
1018
1038
|
systemMicUnavailable = true
|
|
1019
1039
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
1020
|
-
appendLog("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
1021
|
-
scheduleSystemMicAvailabilityRecheck("audio_focus_denied")
|
|
1040
|
+
// appendLog("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
|
|
1022
1041
|
}
|
|
1042
|
+
|
|
1023
1043
|
"permission_denied" -> {
|
|
1024
1044
|
// Microphone permission not granted
|
|
1025
1045
|
systemMicUnavailable = true
|
|
1026
1046
|
Bridge.log("MAN: MIC_UNAVAILABLE: TRUE permission_denied")
|
|
1027
|
-
appendLog("MAN: MIC_UNAVAILABLE: TRUE permission_denied")
|
|
1047
|
+
// appendLog("MAN: MIC_UNAVAILABLE: TRUE permission_denied")
|
|
1028
1048
|
// Don't trigger fallback - need to request permission from user
|
|
1029
1049
|
}
|
|
1050
|
+
|
|
1030
1051
|
"audio_route_changed" -> {
|
|
1031
1052
|
// Audio route changed
|
|
1032
1053
|
// systemMicUnavailable = false
|
|
1033
1054
|
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN audio_route_changed")
|
|
1034
|
-
appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN audio_route_changed")
|
|
1055
|
+
// appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN audio_route_changed")
|
|
1035
1056
|
}
|
|
1057
|
+
|
|
1036
1058
|
"recording_started" -> {
|
|
1037
1059
|
// this is an event from the PhoneMic saying we have started recording
|
|
1038
1060
|
// Audio recording started
|
|
1039
1061
|
// systemMicUnavailable = true
|
|
1040
1062
|
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN recording_started")
|
|
1041
|
-
appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_started")
|
|
1063
|
+
// appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_started")
|
|
1042
1064
|
}
|
|
1065
|
+
|
|
1043
1066
|
"recording_stopped" -> {
|
|
1044
1067
|
// this is an event from the PhoneMic saying we have stopped recording
|
|
1045
1068
|
// Audio recording stopped
|
|
1046
1069
|
// systemMicUnavailable = false
|
|
1047
1070
|
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
1048
|
-
appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
1049
|
-
if (systemMicUnavailable) {
|
|
1050
|
-
scheduleSystemMicAvailabilityRecheck("recording_stopped")
|
|
1051
|
-
}
|
|
1071
|
+
// appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
|
|
1052
1072
|
}
|
|
1073
|
+
|
|
1053
1074
|
else -> {
|
|
1054
1075
|
// Other route changes (headset plug/unplug, BT connect/disconnect, etc.)
|
|
1055
1076
|
// Just log for now - may want to handle these in the future
|
|
1056
1077
|
Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN other: $reason")
|
|
1057
|
-
appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN other: $reason")
|
|
1078
|
+
// appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN other: $reason")
|
|
1058
1079
|
// systemMicUnavailable = false
|
|
1059
1080
|
}
|
|
1060
1081
|
}
|
|
@@ -1099,6 +1120,8 @@ class DeviceManager {
|
|
|
1099
1120
|
sgc = createOptionalMach1Sgc(DeviceTypes.MACH1)
|
|
1100
1121
|
} else if (wearable.contains(DeviceTypes.Z100)) {
|
|
1101
1122
|
sgc = createOptionalMach1Sgc(DeviceTypes.Z100)
|
|
1123
|
+
} else if (wearable.contains(DeviceTypes.NIMO)) {
|
|
1124
|
+
sgc = Nimo()
|
|
1102
1125
|
} else if (wearable.contains(DeviceTypes.FRAME)) {
|
|
1103
1126
|
// sgc = FrameManager()
|
|
1104
1127
|
}
|
|
@@ -1139,7 +1162,49 @@ class DeviceManager {
|
|
|
1139
1162
|
|
|
1140
1163
|
fun restartTranscriber() {
|
|
1141
1164
|
Bridge.log("MAN: Restarting transcriber via command")
|
|
1142
|
-
|
|
1165
|
+
if (ensureTranscriberInitialized()) {
|
|
1166
|
+
transcriber?.restart()
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private fun ensureTranscriberInitialized(): Boolean {
|
|
1171
|
+
if (transcriber != null) return true
|
|
1172
|
+
|
|
1173
|
+
return try {
|
|
1174
|
+
val context = Bridge.getContext()
|
|
1175
|
+
val nextTranscriber = SherpaOnnxTranscriber(context)
|
|
1176
|
+
nextTranscriber.setTranscriptListener(
|
|
1177
|
+
object : SherpaOnnxTranscriber.TranscriptListener {
|
|
1178
|
+
override fun onPartialResult(text: String, language: String) {
|
|
1179
|
+
Bridge.log("STT: Partial result: $text")
|
|
1180
|
+
// The Sherpa model emits all-caps text; lowercase English
|
|
1181
|
+
// output to match the rest of the pipeline (parity with iOS).
|
|
1182
|
+
val formatted = if (language == "en-US") text.lowercase() else text
|
|
1183
|
+
Bridge.sendLocalTranscription(formatted, false, language)
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
override fun onFinalResult(text: String, language: String) {
|
|
1187
|
+
Bridge.log("STT: Final result: $text")
|
|
1188
|
+
// The Sherpa model emits all-caps text; lowercase English
|
|
1189
|
+
// output to match the rest of the pipeline (parity with iOS).
|
|
1190
|
+
val formatted = if (language == "en-US") text.lowercase() else text
|
|
1191
|
+
Bridge.sendLocalTranscription(formatted, true, language)
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
)
|
|
1195
|
+
nextTranscriber.initialize()
|
|
1196
|
+
transcriber = nextTranscriber
|
|
1197
|
+
Bridge.log("SherpaOnnxTranscriber fully initialized")
|
|
1198
|
+
true
|
|
1199
|
+
} catch (e: Exception) {
|
|
1200
|
+
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
1201
|
+
transcriber = null
|
|
1202
|
+
false
|
|
1203
|
+
} catch (e: LinkageError) {
|
|
1204
|
+
Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
|
|
1205
|
+
transcriber = null
|
|
1206
|
+
false
|
|
1207
|
+
}
|
|
1143
1208
|
}
|
|
1144
1209
|
|
|
1145
1210
|
// MARK: - connection state management
|
|
@@ -1168,19 +1233,19 @@ class DeviceManager {
|
|
|
1168
1233
|
|
|
1169
1234
|
// re-apply display height/depth after reconnection
|
|
1170
1235
|
mainHandler.postDelayed(
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1236
|
+
{
|
|
1237
|
+
val h =
|
|
1238
|
+
(DeviceStore.store.get("bluetooth", "dashboard_height") as? Number)
|
|
1239
|
+
?.toInt()
|
|
1240
|
+
?: 4
|
|
1241
|
+
val rawDepth =
|
|
1242
|
+
(DeviceStore.store.get("bluetooth", "dashboard_depth") as? Number)
|
|
1243
|
+
?.toInt()
|
|
1244
|
+
?: dashboardDepth // canonical default (2), not 1
|
|
1245
|
+
val d = rawDepth.coerceIn(1, 4)
|
|
1246
|
+
sgc?.setDashboardPosition(h, d)
|
|
1247
|
+
},
|
|
1248
|
+
2000
|
|
1184
1249
|
)
|
|
1185
1250
|
|
|
1186
1251
|
// Show welcome message on first connect for all display glasses
|
|
@@ -1245,7 +1310,11 @@ class DeviceManager {
|
|
|
1245
1310
|
Bridge.log("MAN: Device disconnected")
|
|
1246
1311
|
lastSystemTimeSyncConnectionKey = ""
|
|
1247
1312
|
DeviceStore.apply("glasses", "headUp", false)
|
|
1248
|
-
DeviceStore.apply(
|
|
1313
|
+
DeviceStore.apply(
|
|
1314
|
+
"glasses",
|
|
1315
|
+
"voiceActivityDetectionEnabled",
|
|
1316
|
+
BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
|
|
1317
|
+
)
|
|
1249
1318
|
}
|
|
1250
1319
|
|
|
1251
1320
|
fun handleControllerReady() {
|
|
@@ -1302,26 +1371,32 @@ class DeviceManager {
|
|
|
1302
1371
|
val title = parsePlaceholders(layout.getString("title", " "))
|
|
1303
1372
|
val data = layout["data"] as? String
|
|
1304
1373
|
|
|
1305
|
-
// Optional
|
|
1374
|
+
// Optional container position/size — used by bitmap_view and positioned_text (G2).
|
|
1306
1375
|
val bmpX = (layout["x"] as? Number)?.toInt()
|
|
1307
1376
|
val bmpY = (layout["y"] as? Number)?.toInt()
|
|
1308
1377
|
val bmpWidth = (layout["width"] as? Number)?.toInt()
|
|
1309
1378
|
val bmpHeight = (layout["height"] as? Number)?.toInt()
|
|
1310
1379
|
|
|
1380
|
+
// Optional positioned_text border (G2).
|
|
1381
|
+
val borderWidth = (layout["borderWidth"] as? Number)?.toInt()
|
|
1382
|
+
val borderRadius = (layout["borderRadius"] as? Number)?.toInt()
|
|
1383
|
+
|
|
1311
1384
|
var newViewState =
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1385
|
+
ViewState(
|
|
1386
|
+
topText,
|
|
1387
|
+
bottomText,
|
|
1388
|
+
title,
|
|
1389
|
+
layoutType ?: "",
|
|
1390
|
+
text,
|
|
1391
|
+
data,
|
|
1392
|
+
null,
|
|
1393
|
+
bmpX,
|
|
1394
|
+
bmpY,
|
|
1395
|
+
bmpWidth,
|
|
1396
|
+
bmpHeight,
|
|
1397
|
+
borderWidth,
|
|
1398
|
+
borderRadius
|
|
1399
|
+
)
|
|
1325
1400
|
|
|
1326
1401
|
val currentState = viewStates[stateIndex]
|
|
1327
1402
|
|
|
@@ -1471,9 +1546,26 @@ class DeviceManager {
|
|
|
1471
1546
|
live.sendCameraFovSetting(requestId, fov, roiPosition)
|
|
1472
1547
|
}
|
|
1473
1548
|
|
|
1474
|
-
fun
|
|
1475
|
-
|
|
1476
|
-
(
|
|
1549
|
+
fun sendCameraTuningConfig(requestId: String, anrOn: Boolean, gainOn: Boolean) {
|
|
1550
|
+
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1551
|
+
live.sendCameraTuningConfig(requestId, anrOn, gainOn)
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
fun warmUpCamera(
|
|
1555
|
+
requestId: String,
|
|
1556
|
+
size: PhotoSize,
|
|
1557
|
+
exposureTimeNs: Long?,
|
|
1558
|
+
durationMs: Int,
|
|
1559
|
+
) {
|
|
1560
|
+
// Fail fast like other camera commands so the SDK promise rejects immediately instead of
|
|
1561
|
+
// hanging until the request timeout with no camera_status.
|
|
1562
|
+
val live =
|
|
1563
|
+
sgc as? MentraLive
|
|
1564
|
+
?: throw BluetoothSdkException(
|
|
1565
|
+
"unsupported_device",
|
|
1566
|
+
"This command requires Mentra Live glasses.",
|
|
1567
|
+
)
|
|
1568
|
+
live.warmUpCamera(requestId, size, exposureTimeNs, durationMs)
|
|
1477
1569
|
}
|
|
1478
1570
|
|
|
1479
1571
|
/**
|
|
@@ -1486,14 +1578,14 @@ class DeviceManager {
|
|
|
1486
1578
|
var result: Map<String, Any>? = null
|
|
1487
1579
|
var error: String? = null
|
|
1488
1580
|
live.getGlassesMediaVolume(
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1581
|
+
{ m ->
|
|
1582
|
+
result = m
|
|
1583
|
+
latch.countDown()
|
|
1584
|
+
},
|
|
1585
|
+
{ e ->
|
|
1586
|
+
error = e
|
|
1587
|
+
latch.countDown()
|
|
1588
|
+
}
|
|
1497
1589
|
)
|
|
1498
1590
|
val completed = latch.await(5, TimeUnit.SECONDS)
|
|
1499
1591
|
if (!completed) {
|
|
@@ -1510,15 +1602,15 @@ class DeviceManager {
|
|
|
1510
1602
|
var result: Map<String, Any>? = null
|
|
1511
1603
|
var error: String? = null
|
|
1512
1604
|
live.setGlassesMediaVolume(
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1605
|
+
level,
|
|
1606
|
+
{ m ->
|
|
1607
|
+
result = m
|
|
1608
|
+
latch.countDown()
|
|
1609
|
+
},
|
|
1610
|
+
{ e ->
|
|
1611
|
+
error = e
|
|
1612
|
+
latch.countDown()
|
|
1613
|
+
}
|
|
1522
1614
|
)
|
|
1523
1615
|
val completed = latch.await(5, TimeUnit.SECONDS)
|
|
1524
1616
|
if (!completed) {
|
|
@@ -1550,26 +1642,27 @@ class DeviceManager {
|
|
|
1550
1642
|
}
|
|
1551
1643
|
|
|
1552
1644
|
fun startVideoRecording(
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1645
|
+
requestId: String,
|
|
1646
|
+
save: Boolean,
|
|
1647
|
+
sound: Boolean,
|
|
1648
|
+
width: Int = 0,
|
|
1649
|
+
height: Int = 0,
|
|
1650
|
+
fps: Int = 0,
|
|
1651
|
+
maxRecordingTimeMinutes: Int = 0,
|
|
1560
1652
|
) {
|
|
1561
1653
|
Bridge.log(
|
|
1562
|
-
|
|
1563
|
-
|
|
1654
|
+
"MAN: onStartVideoRecording: requestId=$requestId, save=$save, sound=$sound, " +
|
|
1655
|
+
"resolution=${width}x${height}@${fps}fps, maxRecordingTimeMinutes=$maxRecordingTimeMinutes"
|
|
1564
1656
|
)
|
|
1565
1657
|
sgc?.startVideoRecording(
|
|
1566
|
-
|
|
1658
|
+
requestId, save, sound, width, height, fps, maxRecordingTimeMinutes
|
|
1659
|
+
)
|
|
1567
1660
|
}
|
|
1568
1661
|
|
|
1569
1662
|
fun stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
1570
1663
|
Bridge.log(
|
|
1571
|
-
|
|
1572
|
-
|
|
1664
|
+
"MAN: onStopVideoRecording: requestId=$requestId, webhook=" +
|
|
1665
|
+
if (webhookUrl.isNullOrEmpty()) "none" else "set"
|
|
1573
1666
|
)
|
|
1574
1667
|
sgc?.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1575
1668
|
}
|
|
@@ -1591,25 +1684,25 @@ class DeviceManager {
|
|
|
1591
1684
|
|
|
1592
1685
|
fun requestPhoto(request: PhotoRequest) {
|
|
1593
1686
|
val exposureNs: Long? =
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
}
|
|
1687
|
+
request.exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
|
|
1688
|
+
when {
|
|
1689
|
+
v > Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
|
|
1690
|
+
else -> v.toLong()
|
|
1599
1691
|
}
|
|
1692
|
+
}
|
|
1600
1693
|
val manualIso = if (exposureNs != null) request.iso?.takeIf { it > 0 } else null
|
|
1601
1694
|
val routed =
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1695
|
+
request.copy(
|
|
1696
|
+
exposureTimeNs = exposureNs?.toDouble(),
|
|
1697
|
+
iso = manualIso,
|
|
1698
|
+
)
|
|
1606
1699
|
Bridge.log(
|
|
1607
|
-
|
|
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"}"
|
|
1608
1701
|
)
|
|
1609
1702
|
val activeSgc = sgc
|
|
1610
1703
|
if (activeSgc == null) {
|
|
1611
1704
|
Bridge.log(
|
|
1612
|
-
|
|
1705
|
+
"MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=${routed.requestId}"
|
|
1613
1706
|
)
|
|
1614
1707
|
return
|
|
1615
1708
|
}
|
|
@@ -1617,13 +1710,13 @@ class DeviceManager {
|
|
|
1617
1710
|
}
|
|
1618
1711
|
|
|
1619
1712
|
fun rgbLedControl(
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1713
|
+
requestId: String,
|
|
1714
|
+
packageName: String?,
|
|
1715
|
+
action: String,
|
|
1716
|
+
color: String?,
|
|
1717
|
+
onDurationMs: Int,
|
|
1718
|
+
offDurationMs: Int,
|
|
1719
|
+
count: Int
|
|
1627
1720
|
) {
|
|
1628
1721
|
Bridge.log("MAN: RGB LED control: action=$action, color=$color, requestId=$requestId")
|
|
1629
1722
|
sgc?.sendRgbLedControl(requestId, packageName, action, color, onDurationMs, offDurationMs, count)
|
|
@@ -1744,7 +1837,11 @@ class DeviceManager {
|
|
|
1744
1837
|
DeviceStore.apply("glasses", "deviceModel", "")
|
|
1745
1838
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
1746
1839
|
DeviceStore.apply("glasses", "connected", false)
|
|
1747
|
-
DeviceStore.apply(
|
|
1840
|
+
DeviceStore.apply(
|
|
1841
|
+
"glasses",
|
|
1842
|
+
"voiceActivityDetectionEnabled",
|
|
1843
|
+
BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
|
|
1844
|
+
)
|
|
1748
1845
|
// disconnect the controller as well:
|
|
1749
1846
|
searchingController = false
|
|
1750
1847
|
DeviceStore.apply("glasses", "controllerConnected", false)
|