@mentra/bluetooth-sdk 0.1.13 → 0.1.16
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 +18 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +48 -48
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +360 -269
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +152 -72
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +12 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +25 -11
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +1 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +1 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +51 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -800
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +423 -117
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +4 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +17 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +3 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -33
- 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 +50 -24
- 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 +14 -8
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +4 -3
- 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 +11 -3
- 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 +70 -61
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +51 -28
- package/ios/Source/Bridge.swift +4 -16
- package/ios/Source/DeviceManager.swift +76 -22
- package/ios/Source/DeviceStore.swift +0 -4
- package/ios/Source/MentraBluetoothSDK.swift +144 -78
- package/ios/Source/OtaManifest.swift +13 -13
- package/ios/Source/camera/CameraModels.swift +82 -20
- package/ios/Source/controllers/ControllerManager.swift +1 -2
- package/ios/Source/controllers/R1.swift +1 -2
- 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/Frame.swift +0 -2
- package/ios/Source/sgcs/G1.swift +52 -4
- package/ios/Source/sgcs/G2.swift +643 -204
- package/ios/Source/sgcs/Mach1.swift +1 -3
- package/ios/Source/sgcs/MentraLive.swift +64 -77
- package/ios/Source/sgcs/MentraNex.swift +1 -3
- package/ios/Source/sgcs/Nimo.swift +1928 -0
- package/ios/Source/sgcs/SGCManager.swift +16 -10
- package/ios/Source/sgcs/Simulated.swift +2 -6
- package/ios/Source/status/DeviceStatus.swift +0 -8
- package/ios/Source/streaming/StreamModels.swift +7 -32
- 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 +6 -1
- package/src/BluetoothSdk.types.ts +54 -28
- package/src/_internal.ts +4 -4
- package/src/_private/BluetoothSdkModule.ts +20 -11
- package/src/_private/cameraRequestPayload.ts +29 -0
- package/src/_private/photoRequestPayload.ts +12 -3
- package/src/index.ts +77 -67
|
@@ -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
|
-
|
|
371
|
-
|
|
372
|
-
if (!micEnabled || !systemMicUnavailable) {
|
|
373
|
-
return
|
|
374
|
-
}
|
|
349
|
+
object : Runnable {
|
|
350
|
+
override fun run() {
|
|
351
|
+
systemMicAvailabilityRecheckRunnable = null
|
|
375
352
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
|
@@ -1365,7 +1440,6 @@ class DeviceManager {
|
|
|
1365
1440
|
|
|
1366
1441
|
fun startStream(message: MutableMap<String, Any>) {
|
|
1367
1442
|
Bridge.log("MAN: startStream")
|
|
1368
|
-
message["flash"] = true
|
|
1369
1443
|
sgc?.startStream(message)
|
|
1370
1444
|
}
|
|
1371
1445
|
|
|
@@ -1435,10 +1509,10 @@ class DeviceManager {
|
|
|
1435
1509
|
}
|
|
1436
1510
|
|
|
1437
1511
|
fun sendButtonPhotoSettings(requestId: String, size: String) {
|
|
1438
|
-
sendButtonPhotoSettings(requestId,
|
|
1512
|
+
sendButtonPhotoSettings(requestId, PhotoCaptureDefaults(PhotoSize.fromValue(size)))
|
|
1439
1513
|
}
|
|
1440
1514
|
|
|
1441
|
-
fun sendButtonPhotoSettings(requestId: String, settings:
|
|
1515
|
+
fun sendButtonPhotoSettings(requestId: String, settings: PhotoCaptureDefaults) {
|
|
1442
1516
|
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1443
1517
|
live.sendButtonPhotoSettings(
|
|
1444
1518
|
requestId,
|
|
@@ -1462,11 +1536,6 @@ class DeviceManager {
|
|
|
1462
1536
|
live.sendButtonVideoRecordingSettings(requestId, width, height, fps)
|
|
1463
1537
|
}
|
|
1464
1538
|
|
|
1465
|
-
fun sendButtonCameraLedSetting(requestId: String, enabled: Boolean) {
|
|
1466
|
-
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1467
|
-
live.sendButtonCameraLedSetting(requestId, enabled)
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
1539
|
fun sendButtonMaxRecordingTime(requestId: String, minutes: Int) {
|
|
1471
1540
|
val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
|
|
1472
1541
|
live.sendButtonMaxRecordingTime(requestId, minutes)
|
|
@@ -1477,9 +1546,26 @@ class DeviceManager {
|
|
|
1477
1546
|
live.sendCameraFovSetting(requestId, fov, roiPosition)
|
|
1478
1547
|
}
|
|
1479
1548
|
|
|
1480
|
-
fun
|
|
1481
|
-
|
|
1482
|
-
(
|
|
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)
|
|
1483
1569
|
}
|
|
1484
1570
|
|
|
1485
1571
|
/**
|
|
@@ -1492,14 +1578,14 @@ class DeviceManager {
|
|
|
1492
1578
|
var result: Map<String, Any>? = null
|
|
1493
1579
|
var error: String? = null
|
|
1494
1580
|
live.getGlassesMediaVolume(
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1581
|
+
{ m ->
|
|
1582
|
+
result = m
|
|
1583
|
+
latch.countDown()
|
|
1584
|
+
},
|
|
1585
|
+
{ e ->
|
|
1586
|
+
error = e
|
|
1587
|
+
latch.countDown()
|
|
1588
|
+
}
|
|
1503
1589
|
)
|
|
1504
1590
|
val completed = latch.await(5, TimeUnit.SECONDS)
|
|
1505
1591
|
if (!completed) {
|
|
@@ -1516,15 +1602,15 @@ class DeviceManager {
|
|
|
1516
1602
|
var result: Map<String, Any>? = null
|
|
1517
1603
|
var error: String? = null
|
|
1518
1604
|
live.setGlassesMediaVolume(
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1605
|
+
level,
|
|
1606
|
+
{ m ->
|
|
1607
|
+
result = m
|
|
1608
|
+
latch.countDown()
|
|
1609
|
+
},
|
|
1610
|
+
{ e ->
|
|
1611
|
+
error = e
|
|
1612
|
+
latch.countDown()
|
|
1613
|
+
}
|
|
1528
1614
|
)
|
|
1529
1615
|
val completed = latch.await(5, TimeUnit.SECONDS)
|
|
1530
1616
|
if (!completed) {
|
|
@@ -1556,26 +1642,27 @@ class DeviceManager {
|
|
|
1556
1642
|
}
|
|
1557
1643
|
|
|
1558
1644
|
fun startVideoRecording(
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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,
|
|
1566
1652
|
) {
|
|
1567
1653
|
Bridge.log(
|
|
1568
|
-
|
|
1569
|
-
|
|
1654
|
+
"MAN: onStartVideoRecording: requestId=$requestId, save=$save, sound=$sound, " +
|
|
1655
|
+
"resolution=${width}x${height}@${fps}fps, maxRecordingTimeMinutes=$maxRecordingTimeMinutes"
|
|
1570
1656
|
)
|
|
1571
1657
|
sgc?.startVideoRecording(
|
|
1572
|
-
|
|
1658
|
+
requestId, save, sound, width, height, fps, maxRecordingTimeMinutes
|
|
1659
|
+
)
|
|
1573
1660
|
}
|
|
1574
1661
|
|
|
1575
1662
|
fun stopVideoRecording(requestId: String, webhookUrl: String?, authToken: String?) {
|
|
1576
1663
|
Bridge.log(
|
|
1577
|
-
|
|
1578
|
-
|
|
1664
|
+
"MAN: onStopVideoRecording: requestId=$requestId, webhook=" +
|
|
1665
|
+
if (webhookUrl.isNullOrEmpty()) "none" else "set"
|
|
1579
1666
|
)
|
|
1580
1667
|
sgc?.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1581
1668
|
}
|
|
@@ -1597,25 +1684,25 @@ class DeviceManager {
|
|
|
1597
1684
|
|
|
1598
1685
|
fun requestPhoto(request: PhotoRequest) {
|
|
1599
1686
|
val exposureNs: Long? =
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
}
|
|
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()
|
|
1605
1691
|
}
|
|
1692
|
+
}
|
|
1606
1693
|
val manualIso = if (exposureNs != null) request.iso?.takeIf { it > 0 } else null
|
|
1607
1694
|
val routed =
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1695
|
+
request.copy(
|
|
1696
|
+
exposureTimeNs = exposureNs?.toDouble(),
|
|
1697
|
+
iso = manualIso,
|
|
1698
|
+
)
|
|
1612
1699
|
Bridge.log(
|
|
1613
|
-
|
|
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"}"
|
|
1614
1701
|
)
|
|
1615
1702
|
val activeSgc = sgc
|
|
1616
1703
|
if (activeSgc == null) {
|
|
1617
1704
|
Bridge.log(
|
|
1618
|
-
|
|
1705
|
+
"MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=${routed.requestId}"
|
|
1619
1706
|
)
|
|
1620
1707
|
return
|
|
1621
1708
|
}
|
|
@@ -1623,13 +1710,13 @@ class DeviceManager {
|
|
|
1623
1710
|
}
|
|
1624
1711
|
|
|
1625
1712
|
fun rgbLedControl(
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1713
|
+
requestId: String,
|
|
1714
|
+
packageName: String?,
|
|
1715
|
+
action: String,
|
|
1716
|
+
color: String?,
|
|
1717
|
+
onDurationMs: Int,
|
|
1718
|
+
offDurationMs: Int,
|
|
1719
|
+
count: Int
|
|
1633
1720
|
) {
|
|
1634
1721
|
Bridge.log("MAN: RGB LED control: action=$action, color=$color, requestId=$requestId")
|
|
1635
1722
|
sgc?.sendRgbLedControl(requestId, packageName, action, color, onDurationMs, offDurationMs, count)
|
|
@@ -1750,7 +1837,11 @@ class DeviceManager {
|
|
|
1750
1837
|
DeviceStore.apply("glasses", "deviceModel", "")
|
|
1751
1838
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
1752
1839
|
DeviceStore.apply("glasses", "connected", false)
|
|
1753
|
-
DeviceStore.apply(
|
|
1840
|
+
DeviceStore.apply(
|
|
1841
|
+
"glasses",
|
|
1842
|
+
"voiceActivityDetectionEnabled",
|
|
1843
|
+
BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
|
|
1844
|
+
)
|
|
1754
1845
|
// disconnect the controller as well:
|
|
1755
1846
|
searchingController = false
|
|
1756
1847
|
DeviceStore.apply("glasses", "controllerConnected", false)
|