@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.
Files changed (67) hide show
  1. package/README.md +4 -6
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +22 -9
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +358 -261
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -3
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +133 -52
  7. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +12 -12
  8. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +22 -6
  9. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +50 -3
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -795
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +422 -78
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +3 -4
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -1
  16. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +5 -3
  17. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
  18. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +60 -1
  19. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
  20. package/build/BluetoothSdk.types.d.ts +39 -14
  21. package/build/BluetoothSdk.types.d.ts.map +1 -1
  22. package/build/BluetoothSdk.types.js +1 -0
  23. package/build/BluetoothSdk.types.js.map +1 -1
  24. package/build/_internal.d.ts +4 -4
  25. package/build/_internal.js +4 -4
  26. package/build/_internal.js.map +1 -1
  27. package/build/_private/BluetoothSdkModule.d.ts +11 -3
  28. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  29. package/build/_private/BluetoothSdkModule.js +5 -0
  30. package/build/_private/BluetoothSdkModule.js.map +1 -1
  31. package/build/_private/cameraRequestPayload.d.ts +4 -0
  32. package/build/_private/cameraRequestPayload.d.ts.map +1 -0
  33. package/build/_private/cameraRequestPayload.js +25 -0
  34. package/build/_private/cameraRequestPayload.js.map +1 -0
  35. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  36. package/build/_private/photoRequestPayload.js +8 -2
  37. package/build/_private/photoRequestPayload.js.map +1 -1
  38. package/build/index.d.ts +1 -1
  39. package/build/index.d.ts.map +1 -1
  40. package/build/index.js +72 -54
  41. package/build/index.js.map +1 -1
  42. package/ios/BluetoothSdkModule.swift +40 -12
  43. package/ios/Source/BluetoothSdkDefaults.swift +2 -30
  44. package/ios/Source/Bridge.swift +4 -16
  45. package/ios/Source/DeviceManager.swift +72 -12
  46. package/ios/Source/MentraBluetoothSDK.swift +120 -50
  47. package/ios/Source/OtaManifest.swift +13 -13
  48. package/ios/Source/camera/CameraModels.swift +75 -9
  49. package/ios/Source/errors/{BluetoothError.swift → BluetoothSdkError.swift} +1 -1
  50. package/ios/Source/events/BluetoothEvents.swift +5 -27
  51. package/ios/Source/internal/BluetoothAvailability.swift +5 -5
  52. package/ios/Source/sgcs/G1.swift +51 -1
  53. package/ios/Source/sgcs/G2.swift +643 -203
  54. package/ios/Source/sgcs/MentraLive.swift +53 -39
  55. package/ios/Source/sgcs/Nimo.swift +1928 -0
  56. package/ios/Source/sgcs/SGCManager.swift +12 -5
  57. package/ios/Source/stt/STTTools.swift +1 -1
  58. package/ios/Source/types/DeviceModels.swift +5 -0
  59. package/ios/Source/utils/Constants.swift +2 -0
  60. package/package.json +9 -1
  61. package/scripts/inject-ios-sdk-version.mjs +65 -0
  62. package/src/BluetoothSdk.types.ts +42 -17
  63. package/src/_internal.ts +4 -4
  64. package/src/_private/BluetoothSdkModule.ts +18 -2
  65. package/src/_private/cameraRequestPayload.ts +29 -0
  66. package/src/_private/photoRequestPayload.ts +9 -2
  67. 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 private var _instance: DeviceManager? = null
45
+ @Volatile
46
+ private var _instance: DeviceManager? = null
45
47
 
46
48
  @JvmStatic
47
49
  fun getInstance(): DeviceManager {
48
50
  return _instance
49
- ?: synchronized(this) { _instance ?: DeviceManager().also { _instance = it } }
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
- (DeviceStore.store.get("bluetooth", "micRanking") as? List<*>)
193
- ?.mapNotNull { it as? String }
194
- ?.toMutableList()
195
- ?: MicMap.map["auto"]?.toMutableList() ?: mutableListOf()
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
- // Initialize local STT transcriber
271
- try {
272
- val context = Bridge.getContext()
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
- object : Runnable {
336
- override fun run() {
337
- checkAndReinitGlassesMic()
338
- mainHandler.postDelayed(this, 10_000)
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
- object : Runnable {
369
- override fun run() {
370
- systemMicAvailabilityRecheckRunnable = null
349
+ object : Runnable {
350
+ override fun run() {
351
+ systemMicAvailabilityRecheckRunnable = null
371
352
 
372
- if (!micEnabled || !systemMicUnavailable) {
373
- return
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
- systemMicUnavailable = false
383
- Bridge.log("MAN: MIC_UNAVAILABLE: FALSE recheck_after_$reason")
384
- appendLog("MAN: MIC_UNAVAILABLE: FALSE recheck_after_$reason")
385
- updateMicState()
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
- "MAN: Initial permissions - BT: $lastHadBluetoothPermission, Mic: $lastHadMicrophonePermission"
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
- object : BroadcastReceiver() {
408
- override fun onReceive(context: Context?, intent: Intent?) {
409
- if (intent?.action == Intent.ACTION_PACKAGE_CHANGED &&
410
- intent.data?.schemeSpecificPart == context?.packageName
411
- ) {
412
-
413
- Bridge.log("MAN: Package changed, checking permissions...")
414
- checkPermissionChanges()
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
- IntentFilter().apply {
423
- addAction(Intent.ACTION_PACKAGE_CHANGED)
424
- addDataScheme("package")
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
- object : Runnable {
439
- override fun run() {
440
- checkPermissionChanges()
441
- handler.postDelayed(this, 10000) // Check every 10 seconds
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
- "MAN: Bluetooth permission changed: $lastHadBluetoothPermission -> $currentHasBluetoothPermission"
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
- "MAN: Microphone permission changed: $lastHadMicrophonePermission -> $currentHasMicrophonePermission"
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
- context,
486
- android.Manifest.permission.BLUETOOTH_CONNECT
466
+ context,
467
+ android.Manifest.permission.BLUETOOTH_CONNECT
487
468
  ) == PackageManager.PERMISSION_GRANTED
488
469
  val scan = ContextCompat.checkSelfPermission(
489
- context,
490
- android.Manifest.permission.BLUETOOTH_SCAN
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
- PackageManager.PERMISSION_GRANTED
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
- context,
506
- android.Manifest.permission.RECORD_AUDIO
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
- object : BroadcastReceiver() {
515
- override fun onReceive(context: Context?, intent: Intent?) {
516
- if (intent?.action != BluetoothAdapter.ACTION_STATE_CHANGED) return
517
-
518
- val state =
519
- intent.getIntExtra(
520
- BluetoothAdapter.EXTRA_STATE,
521
- BluetoothAdapter.ERROR
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
- when (state) {
524
- BluetoothAdapter.STATE_OFF -> {
525
- Bridge.log("MAN: Bluetooth turned OFF (control center or settings)")
526
- disconnect()
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
- ViewState(
624
- " ",
625
- " ",
626
- " ",
627
- "text_wall",
628
- "\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
629
- null,
630
- null
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
- ViewState(
636
- " ",
637
- " ",
638
- " ",
639
- "text_wall",
640
- "\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
641
- null,
642
- null
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
- "${s1.layoutType}${s1.text}${s1.topText}${s1.bottomText}${s1.title}${s1.data ?: ""}"
633
+ "${s1.layoutType}${s1.text}${s1.topText}${s1.bottomText}${s1.title}${s1.data ?: ""}"
650
634
  val state2 =
651
- "${s2.layoutType}${s2.text}${s2.topText}${s2.bottomText}${s2.title}${s2.data ?: ""}"
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
- var topText: String,
663
- var bottomText: String,
664
- var title: String,
665
- var layoutType: String,
666
- var text: String,
667
- var data: String?,
668
- var animationData: Map<String, Any>?,
669
- // Optional bitmap_view container position/size (used by G2; ignored by others)
670
- var bmpX: Int? = null,
671
- var bmpY: Int? = null,
672
- var bmpWidth: Int? = null,
673
- var bmpHeight: Int? = null
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
- transcriber?.acceptAudio(pcmData)
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
- micMode == MicTypes.BLUETOOTH_CLASSIC ||
789
- micMode == MicTypes.BLUETOOTH
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
- micMode == MicTypes.BLUETOOTH_CLASSIC ||
852
- micMode == MicTypes.BLUETOOTH
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
- data,
925
- currentViewState.bmpX,
926
- currentViewState.bmpY,
927
- currentViewState.bmpWidth,
928
- currentViewState.bmpHeight
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
- mapOf(
953
- "\$no_datetime$" to formattedDate,
954
- "\$DATE$" to currentDate,
955
- "\$TIME12$" to time12,
956
- "\$TIME24$" to time24,
957
- "\$GBATT$" to
958
- (sgc?.batteryLevel?.let { if (it == -1) "" else "$it%" } ?: ""),
959
- "\$CONNECTION_STATUS$" to "Connected"
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
- transcriber?.restart()
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
- val h =
1173
- (DeviceStore.store.get("bluetooth", "dashboard_height") as? Number)
1174
- ?.toInt()
1175
- ?: 4
1176
- val rawDepth =
1177
- (DeviceStore.store.get("bluetooth", "dashboard_depth") as? Number)
1178
- ?.toInt()
1179
- ?: dashboardDepth // canonical default (2), not 1
1180
- val d = rawDepth.coerceIn(1, 4)
1181
- sgc?.setDashboardPosition(h, d)
1182
- },
1183
- 2000
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("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
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 bitmap_view container position/size (forwarded to the SGC; used by G2).
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
- ViewState(
1313
- topText,
1314
- bottomText,
1315
- title,
1316
- layoutType ?: "",
1317
- text,
1318
- data,
1319
- null,
1320
- bmpX,
1321
- bmpY,
1322
- bmpWidth,
1323
- bmpHeight
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 retryOtaVersionCheck() {
1475
- Bridge.log("MAN: Retrying glasses OTA version check after clock sync")
1476
- (sgc as? MentraLive)?.sendOtaRetryVersionCheck()
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
- { m ->
1490
- result = m
1491
- latch.countDown()
1492
- },
1493
- { e ->
1494
- error = e
1495
- latch.countDown()
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
- level,
1514
- { m ->
1515
- result = m
1516
- latch.countDown()
1517
- },
1518
- { e ->
1519
- error = e
1520
- latch.countDown()
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
- requestId: String,
1554
- save: Boolean,
1555
- sound: Boolean,
1556
- width: Int = 0,
1557
- height: Int = 0,
1558
- fps: Int = 0,
1559
- maxRecordingTimeMinutes: Int = 0,
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
- "MAN: onStartVideoRecording: requestId=$requestId, save=$save, sound=$sound, " +
1563
- "resolution=${width}x${height}@${fps}fps, maxRecordingTimeMinutes=$maxRecordingTimeMinutes"
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
- requestId, save, sound, width, height, fps, maxRecordingTimeMinutes)
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
- "MAN: onStopVideoRecording: requestId=$requestId, webhook=" +
1572
- if (webhookUrl.isNullOrEmpty()) "none" else "set"
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
- request.exposureTimeNs?.takeIf { it.isFinite() && it > 0 }?.let { v ->
1595
- when {
1596
- v > Long.MAX_VALUE.toDouble() -> Long.MAX_VALUE
1597
- else -> v.toLong()
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
- request.copy(
1603
- exposureTimeNs = exposureNs?.toDouble(),
1604
- iso = manualIso,
1605
- )
1695
+ request.copy(
1696
+ exposureTimeNs = exposureNs?.toDouble(),
1697
+ iso = manualIso,
1698
+ )
1606
1699
  Bridge.log(
1607
- "MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=${routed.requestId} appId=${routed.appId} 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"}"
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
- "MAN: PHOTO PIPELINE — sgc is null (glasses not connected); dropping requestId=${routed.requestId}"
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
- requestId: String,
1621
- packageName: String?,
1622
- action: String,
1623
- color: String?,
1624
- onDurationMs: Int,
1625
- offDurationMs: Int,
1626
- count: Int
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("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
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)