@mentra/bluetooth-sdk 0.1.19 → 0.1.20

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 (43) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +135 -51
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +12 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +13 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
  8. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +113 -147
  10. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -3
  11. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -8
  12. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +6 -5
  13. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
  14. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +16 -0
  15. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
  16. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
  17. package/build/BluetoothSdk.types.d.ts +25 -0
  18. package/build/BluetoothSdk.types.d.ts.map +1 -1
  19. package/build/BluetoothSdk.types.js.map +1 -1
  20. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  21. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  22. package/build/_private/BluetoothSdkModule.js.map +1 -1
  23. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  24. package/build/_private/photoRequestPayload.js +1 -0
  25. package/build/_private/photoRequestPayload.js.map +1 -1
  26. package/build/index.d.ts +1 -1
  27. package/build/index.d.ts.map +1 -1
  28. package/build/index.js +5 -0
  29. package/build/index.js.map +1 -1
  30. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  31. package/ios/Source/DeviceManager.swift +2 -1
  32. package/ios/Source/DeviceStore.swift +5 -0
  33. package/ios/Source/MentraBluetoothSDK.swift +14 -1
  34. package/ios/Source/camera/CameraModels.swift +17 -3
  35. package/ios/Source/sgcs/G1.swift +1 -1
  36. package/ios/Source/sgcs/MentraLive.swift +140 -17
  37. package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
  38. package/ios/Source/utils/G1Text.swift +30 -1
  39. package/package.json +1 -1
  40. package/src/BluetoothSdk.types.ts +26 -0
  41. package/src/_private/BluetoothSdkModule.ts +4 -0
  42. package/src/_private/photoRequestPayload.ts +1 -0
  43. package/src/index.ts +6 -0
package/README.md CHANGED
@@ -475,7 +475,7 @@ For bare native iOS apps, use the public SwiftPM repository:
475
475
  https://github.com/Mentra-Community/mentra-bluetooth-sdk-ios.git
476
476
  ```
477
477
 
478
- Select version `0.1.19`, then add the `MentraBluetoothSDK` product to your app target.
478
+ Select version `0.1.20`, then add the `MentraBluetoothSDK` product to your app target.
479
479
 
480
480
  For local SDK development, add this package folder directly in Xcode:
481
481
 
@@ -1,13 +1,92 @@
1
+ @file:Suppress("FunctionName")
2
+
1
3
  package com.mentra.bluetoothsdk
2
4
 
3
5
  import com.mentra.bluetoothsdk.debug.BleTraceLogger
4
6
  import com.mentra.bluetoothsdk.utils.DeviceTypes
7
+ import expo.modules.kotlin.exception.CodedException
5
8
  import expo.modules.kotlin.functions.Coroutine
6
9
  import expo.modules.kotlin.modules.Module
7
10
  import expo.modules.kotlin.modules.ModuleDefinition
11
+ import expo.modules.kotlin.modules.ModuleDefinitionBuilder
8
12
  import kotlinx.coroutines.Dispatchers
9
13
  import kotlinx.coroutines.withContext
10
14
 
15
+ // Expo has no module-level error mapper, so SDK-backed registrations translate
16
+ // core exceptions here while keeping Expo types out of the native SDK API.
17
+ private inline fun <T> withExpoSdkError(block: () -> T): T =
18
+ try {
19
+ block()
20
+ } catch (error: BluetoothSdkException) {
21
+ throw CodedException(error.code, error.message ?: error.code, error)
22
+ }
23
+
24
+ private suspend inline fun <T> withExpoSdkErrorSuspend(crossinline block: suspend () -> T): T =
25
+ try {
26
+ block()
27
+ } catch (error: BluetoothSdkException) {
28
+ throw CodedException(error.code, error.message ?: error.code, error)
29
+ }
30
+
31
+ private inline fun <reified R> ModuleDefinitionBuilder.SdkAsyncFunction(
32
+ name: String,
33
+ crossinline body: () -> R,
34
+ ) = AsyncFunction<R>(name) { withExpoSdkError { body() } }
35
+
36
+ private inline fun <reified R, reified P0> ModuleDefinitionBuilder.SdkAsyncFunction(
37
+ name: String,
38
+ crossinline body: (P0) -> R,
39
+ ) = AsyncFunction<R, P0>(name) { p0 -> withExpoSdkError { body(p0) } }
40
+
41
+ private inline fun <reified R, reified P0, reified P1> ModuleDefinitionBuilder.SdkAsyncFunction(
42
+ name: String,
43
+ crossinline body: (P0, P1) -> R,
44
+ ) = AsyncFunction<R, P0, P1>(name) { p0, p1 -> withExpoSdkError { body(p0, p1) } }
45
+
46
+ private inline fun <reified R, reified P0, reified P1, reified P2> ModuleDefinitionBuilder.SdkAsyncFunction(
47
+ name: String,
48
+ crossinline body: (P0, P1, P2) -> R,
49
+ ) = AsyncFunction<R, P0, P1, P2>(name) { p0, p1, p2 -> withExpoSdkError { body(p0, p1, p2) } }
50
+
51
+ private inline fun <reified R, reified P0, reified P1, reified P2, reified P3>
52
+ ModuleDefinitionBuilder.SdkAsyncFunction(
53
+ name: String,
54
+ crossinline body: (P0, P1, P2, P3) -> R,
55
+ ) = AsyncFunction<R, P0, P1, P2, P3>(name) { p0, p1, p2, p3 ->
56
+ withExpoSdkError { body(p0, p1, p2, p3) }
57
+ }
58
+
59
+ private inline fun <
60
+ reified R,
61
+ reified P0,
62
+ reified P1,
63
+ reified P2,
64
+ reified P3,
65
+ reified P4,
66
+ reified P5,
67
+ reified P6,
68
+ > ModuleDefinitionBuilder.SdkAsyncFunction(
69
+ name: String,
70
+ crossinline body: (P0, P1, P2, P3, P4, P5, P6) -> R,
71
+ ) = AsyncFunction<R, P0, P1, P2, P3, P4, P5, P6>(name) { p0, p1, p2, p3, p4, p5, p6 ->
72
+ withExpoSdkError { body(p0, p1, p2, p3, p4, p5, p6) }
73
+ }
74
+
75
+ private inline fun <reified R> ModuleDefinitionBuilder.SdkFunction(
76
+ name: String,
77
+ crossinline body: () -> R,
78
+ ) = Function<R>(name) { withExpoSdkError { body() } }
79
+
80
+ private inline fun <reified R, reified P0> ModuleDefinitionBuilder.SdkFunction(
81
+ name: String,
82
+ crossinline body: (P0) -> R,
83
+ ) = Function<R, P0>(name) { p0 -> withExpoSdkError { body(p0) } }
84
+
85
+ private inline fun <reified R> ModuleDefinitionBuilder.SdkCoroutineFunction(
86
+ name: String,
87
+ crossinline body: suspend () -> R,
88
+ ) = AsyncFunction(name) Coroutine { -> withExpoSdkErrorSuspend { body() } }
89
+
11
90
  class BluetoothSdkModule : Module() {
12
91
  private var sdk: MentraBluetoothSdk? = null
13
92
  private var deviceManager: DeviceManager? = null
@@ -183,9 +262,10 @@ class BluetoothSdkModule : Module() {
183
262
 
184
263
  private fun requireSdk(): MentraBluetoothSdk =
185
264
  sdk
186
- ?: throw BluetoothSdkException(
265
+ ?: throw CodedException(
187
266
  "sdk_not_initialized",
188
267
  "Bluetooth SDK is not initialized.",
268
+ null,
189
269
  )
190
270
 
191
271
  override fun definition() = ModuleDefinition {
@@ -335,11 +415,11 @@ class BluetoothSdkModule : Module() {
335
415
 
336
416
  // MARK: - Display Commands
337
417
 
338
- AsyncFunction("displayEvent") { params: Map<String, Any> ->
418
+ SdkAsyncFunction("displayEvent") { params: Map<String, Any> ->
339
419
  sdk?.displayEvent(DisplayEventRequest(params))
340
420
  }
341
421
 
342
- AsyncFunction("displayText") { text: String, x: Int?, y: Int?, size: Int? ->
422
+ SdkAsyncFunction("displayText") { text: String, x: Int?, y: Int?, size: Int? ->
343
423
  sdk?.displayText(
344
424
  text = text,
345
425
  x = x ?: 0,
@@ -348,34 +428,35 @@ class BluetoothSdkModule : Module() {
348
428
  )
349
429
  }
350
430
 
351
- AsyncFunction("clearDisplay") { sdk?.clearDisplay() }
431
+ SdkAsyncFunction("clearDisplay") { -> sdk?.clearDisplay() }
352
432
 
353
433
  // MARK: - Connection Commands
354
434
 
355
- AsyncFunction("connectDefault") { sdk?.connectDefault() }
435
+ SdkAsyncFunction("connectDefault") { -> sdk?.connectDefault() }
356
436
 
357
- AsyncFunction("connectDefaultWithOptions") { options: Map<String, Any> ->
437
+ SdkAsyncFunction("connectDefaultWithOptions") { options: Map<String, Any> ->
358
438
  sdk?.connectDefault(options.toMentraConnectOptions())
359
439
  }
360
440
 
361
- AsyncFunction("setDefaultDevice") { device: Map<String, Any>? ->
441
+ SdkAsyncFunction("setDefaultDevice") { device: Map<String, Any>? ->
362
442
  sdk?.setDefaultDevice(device.toMentraDevice())
363
443
  }
364
444
 
365
- AsyncFunction("clearDefaultDevice") { sdk?.clearDefaultDevice() }
445
+ SdkAsyncFunction("clearDefaultDevice") { -> sdk?.clearDefaultDevice() }
366
446
 
367
- AsyncFunction("connectWithOptions") { device: Map<String, Any>, options: Map<String, Any> ->
447
+ SdkAsyncFunction("connectWithOptions") { device: Map<String, Any>, options: Map<String, Any> ->
368
448
  sdk?.connect(
369
- device.toMentraDevice() ?: throw IllegalArgumentException("connect requires a Device with model and name."),
449
+ device.toMentraDevice()
450
+ ?: throw IllegalArgumentException("connect requires a Device with model and name."),
370
451
  options.toMentraConnectOptions(),
371
452
  )
372
453
  }
373
454
 
374
- AsyncFunction("connectSimulated") { sdk?.connectSimulated() }
455
+ SdkAsyncFunction("connectSimulated") { -> sdk?.connectSimulated() }
375
456
 
376
- AsyncFunction("disconnect") { sdk?.disconnect() }
457
+ SdkAsyncFunction("disconnect") { -> sdk?.disconnect() }
377
458
 
378
- AsyncFunction("forget") { sdk?.forget() }
459
+ SdkAsyncFunction("forget") { -> sdk?.forget() }
379
460
 
380
461
  AsyncFunction("connectDefaultController") { deviceManager?.connectDefaultController() }
381
462
 
@@ -383,15 +464,15 @@ class BluetoothSdkModule : Module() {
383
464
 
384
465
  AsyncFunction("forgetController") { deviceManager?.forgetController() }
385
466
 
386
- AsyncFunction("startScan") { model: String ->
467
+ SdkAsyncFunction("startScan") { model: String ->
387
468
  sdk?.startScan(DeviceModel.fromDeviceType(model))
388
469
  }
389
470
 
390
- AsyncFunction("stopScan") { sdk?.stopScan() }
471
+ SdkAsyncFunction("stopScan") { -> sdk?.stopScan() }
391
472
 
392
- AsyncFunction("cancelConnectionAttempt") { sdk?.cancelConnectionAttempt() }
473
+ SdkAsyncFunction("cancelConnectionAttempt") { -> sdk?.cancelConnectionAttempt() }
393
474
 
394
- AsyncFunction("showDashboard") { sdk?.showDashboard() }
475
+ SdkAsyncFunction("showDashboard") { -> sdk?.showDashboard() }
395
476
 
396
477
  AsyncFunction("ping") { deviceManager?.ping() }
397
478
 
@@ -409,51 +490,54 @@ class BluetoothSdkModule : Module() {
409
490
 
410
491
  // MARK: - Incident Reporting
411
492
 
412
- AsyncFunction("sendIncidentId") { incidentId: String, apiBaseUrl: String? ->
493
+ SdkAsyncFunction("sendIncidentId") { incidentId: String, apiBaseUrl: String? ->
413
494
  sdk?.sendIncidentId(incidentId, apiBaseUrl)
414
495
  }
415
496
 
416
497
  // MARK: - WiFi Commands
417
498
 
418
- AsyncFunction("requestWifiScan") { requireSdk().requestWifiScan().map { it.toMap() } }
499
+ SdkAsyncFunction("requestWifiScan") { -> requireSdk().requestWifiScan().map { it.toMap() } }
419
500
 
420
- AsyncFunction("sendWifiCredentials") { ssid: String, password: String ->
501
+ SdkAsyncFunction("sendWifiCredentials") { ssid: String, password: String ->
421
502
  requireSdk().sendWifiCredentials(ssid, password).values
422
503
  }
423
504
 
424
- AsyncFunction("forgetWifiNetwork") { ssid: String -> requireSdk().forgetWifiNetwork(ssid).values }
505
+ SdkAsyncFunction("forgetWifiNetwork") { ssid: String ->
506
+ requireSdk().forgetWifiNetwork(ssid).values
507
+ }
425
508
 
426
- AsyncFunction("setHotspotState") { enabled: Boolean ->
509
+ SdkAsyncFunction("setHotspotState") { enabled: Boolean ->
427
510
  requireSdk().setHotspotState(enabled).values
428
511
  }
429
512
 
430
- AsyncFunction("setSystemTime") { timestampMs: Double ->
513
+ SdkAsyncFunction("setSystemTime") { timestampMs: Double ->
431
514
  sdk?.setSystemTime(timestampMs.toLong())
432
515
  }
433
516
 
434
517
  // MARK: - Gallery Commands
435
518
 
436
- AsyncFunction("setGalleryModeEnabled") { enabled: Boolean ->
519
+ SdkAsyncFunction("setGalleryModeEnabled") { enabled: Boolean ->
437
520
  requireSdk().setGalleryModeEnabled(enabled).values
438
521
  }
439
522
 
440
- AsyncFunction("setVoiceActivityDetectionEnabled") { enabled: Boolean ->
523
+ SdkAsyncFunction("setVoiceActivityDetectionEnabled") { enabled: Boolean ->
441
524
  sdk?.setVoiceActivityDetectionEnabled(enabled)
442
525
  }
443
526
 
444
- AsyncFunction("setPhotoCaptureDefaults") { params: Map<String, Any?> ->
527
+ @Suppress("DEPRECATION")
528
+ SdkAsyncFunction("setPhotoCaptureDefaults") { params: Map<String, Any?> ->
445
529
  requireSdk().setPhotoCaptureDefaults(params.toPhotoCaptureDefaults()).values
446
530
  }
447
531
 
448
- AsyncFunction("setVideoRecordingDefaults") { width: Int, height: Int, fps: Int ->
532
+ SdkAsyncFunction("setVideoRecordingDefaults") { width: Int, height: Int, fps: Int ->
449
533
  requireSdk().setVideoRecordingDefaults(VideoRecordingDefaults(width, height, fps)).values
450
534
  }
451
535
 
452
- AsyncFunction("setMaxVideoRecordingDuration") { minutes: Int ->
536
+ SdkAsyncFunction("setMaxVideoRecordingDuration") { minutes: Int ->
453
537
  requireSdk().setMaxVideoRecordingDuration(minutes).values
454
538
  }
455
539
 
456
- AsyncFunction("setCameraFov") { fov: Map<String, Any> ->
540
+ SdkAsyncFunction("setCameraFov") { fov: Map<String, Any> ->
457
541
  val value = (fov["fov"] as? Number)?.toInt() ?: CameraFov.DEFAULT_FOV
458
542
  val roiPosition = CameraRoiPosition.fromValue(
459
543
  (fov["roiPosition"] as? Number)?.toInt()
@@ -462,13 +546,13 @@ class BluetoothSdkModule : Module() {
462
546
  requireSdk().setCameraFov(CameraFov(value, roiPosition)).values
463
547
  }
464
548
 
465
- AsyncFunction("setCameraTuningConfig") { anrOn: Boolean, gainOn: Boolean ->
549
+ SdkAsyncFunction("setCameraTuningConfig") { anrOn: Boolean, gainOn: Boolean ->
466
550
  requireSdk().setCameraTuningConfig(anrOn, gainOn).values
467
551
  }
468
552
 
469
- AsyncFunction("queryGalleryStatus") { requireSdk().queryGalleryStatus().values }
553
+ SdkAsyncFunction("queryGalleryStatus") { -> requireSdk().queryGalleryStatus().values }
470
554
 
471
- AsyncFunction("requestPhoto") { params: Map<String, Any?> ->
555
+ SdkAsyncFunction("requestPhoto") { params: Map<String, Any?> ->
472
556
  // JS may pass null for optional fields; Map<String, Any> rejects null values at the bridge.
473
557
  val sanitized =
474
558
  params.mapNotNull { (key, value) ->
@@ -476,12 +560,12 @@ class BluetoothSdkModule : Module() {
476
560
  }.toMap()
477
561
  val req = PhotoRequest.fromMap(sanitized)
478
562
  Bridge.log(
479
- "NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} size=${req.size} compress=${req.compress} sound=${req.sound} exposureTimeNs=${req.exposureTimeNs} iso=${req.iso}"
563
+ "NATIVE: PHOTO PIPELINE [3/6] BluetoothSdk.requestPhoto requestId=${req.requestId} size=${req.size} mode=${req.mode.value} compress=${req.compress} sound=${req.sound} exposureTimeNs=${req.exposureTimeNs} iso=${req.iso}"
480
564
  )
481
565
  requireSdk().requestPhoto(req).values
482
566
  }
483
567
 
484
- AsyncFunction("warmUpCamera") { params: Map<String, Any?> ->
568
+ SdkAsyncFunction("warmUpCamera") { params: Map<String, Any?> ->
485
569
  val requestId = params["requestId"] as? String
486
570
  val size = PhotoSize.fromValue(params["size"] as? String)
487
571
  val exposureRaw = (params["exposureTimeNs"] as? Number)?.toDouble()
@@ -494,19 +578,19 @@ class BluetoothSdkModule : Module() {
494
578
 
495
579
  // MARK: - OTA Commands
496
580
 
497
- Function("setOtaVersionUrl") { otaVersionUrl: String ->
581
+ SdkFunction("setOtaVersionUrl") { otaVersionUrl: String ->
498
582
  requireSdk().setOtaVersionUrl(otaVersionUrl)
499
583
  }
500
584
 
501
- Function("getOtaVersionUrl") { requireSdk().getOtaVersionUrl() }
585
+ SdkFunction("getOtaVersionUrl") { -> requireSdk().getOtaVersionUrl() }
502
586
 
503
587
  // Runs on Dispatchers.IO, not the shared Expo AsyncFunctionQueue:
504
588
  // manifest fetches and version waits can block for several seconds.
505
- AsyncFunction("checkForOtaUpdate") Coroutine { ->
589
+ SdkCoroutineFunction("checkForOtaUpdate") {
506
590
  withContext(Dispatchers.IO) { requireSdk().checkForOtaUpdate() }
507
591
  }
508
592
 
509
- AsyncFunction("startOtaUpdate") { otaVersionUrl: String? ->
593
+ SdkAsyncFunction("startOtaUpdate") { otaVersionUrl: String? ->
510
594
  val sdk = requireSdk()
511
595
  if (otaVersionUrl.isNullOrBlank()) {
512
596
  sdk.startOtaUpdate().values
@@ -515,21 +599,21 @@ class BluetoothSdkModule : Module() {
515
599
  }
516
600
  }
517
601
 
518
- AsyncFunction("sendOtaQueryStatus") { requireSdk().sendOtaQueryStatus().values }
602
+ SdkAsyncFunction("sendOtaQueryStatus") { -> requireSdk().sendOtaQueryStatus().values }
519
603
 
520
604
  // MARK: - Version Info Commands
521
605
 
522
- AsyncFunction("requestVersionInfo") { requireSdk().requestVersionInfo().toMap() }
606
+ SdkAsyncFunction("requestVersionInfo") { -> requireSdk().requestVersionInfo().toMap() }
523
607
 
524
608
  // MARK: - Power Control Commands
525
609
 
526
- AsyncFunction("sendShutdown") { sdk?.sendShutdown() }
610
+ SdkAsyncFunction("sendShutdown") { -> sdk?.sendShutdown() }
527
611
 
528
- AsyncFunction("sendReboot") { sdk?.sendReboot() }
612
+ SdkAsyncFunction("sendReboot") { -> sdk?.sendReboot() }
529
613
 
530
614
  // MARK: - Video Recording Commands
531
615
 
532
- AsyncFunction("startVideoRecording") {
616
+ SdkAsyncFunction("startVideoRecording") {
533
617
  requestId: String,
534
618
  save: Boolean,
535
619
  sound: Boolean,
@@ -553,7 +637,7 @@ class BluetoothSdkModule : Module() {
553
637
 
554
638
  // webhookUrl/authToken are supplied at stop (not start) so the token is
555
639
  // fresh when the upload runs. Empty/null webhook = keep on device.
556
- AsyncFunction("stopVideoRecording") {
640
+ SdkAsyncFunction("stopVideoRecording") {
557
641
  requestId: String,
558
642
  webhookUrl: String?,
559
643
  authToken: String? ->
@@ -562,23 +646,23 @@ class BluetoothSdkModule : Module() {
562
646
 
563
647
  // MARK: - Stream Commands
564
648
 
565
- AsyncFunction("startStream") { params: Map<String, Any> ->
649
+ SdkAsyncFunction("startStream") { params: Map<String, Any> ->
566
650
  requireSdk().startStream(StreamRequest.fromMap(params)).values
567
651
  }
568
652
 
569
- AsyncFunction("startExternallyManagedStream") { params: Map<String, Any> ->
653
+ SdkAsyncFunction("startExternallyManagedStream") { params: Map<String, Any> ->
570
654
  requireSdk().startExternallyManagedStream(StreamRequest.fromMap(params)).values
571
655
  }
572
656
 
573
- AsyncFunction("stopStream") { requireSdk().stopStream().values }
657
+ SdkAsyncFunction("stopStream") { -> requireSdk().stopStream().values }
574
658
 
575
- AsyncFunction("sendExternallyManagedStreamKeepAlive") { params: Map<String, Any> ->
659
+ SdkAsyncFunction("sendExternallyManagedStreamKeepAlive") { params: Map<String, Any> ->
576
660
  sdk?.sendExternallyManagedStreamKeepAlive(StreamKeepAliveRequest.fromMap(params))
577
661
  }
578
662
 
579
663
  // MARK: - Microphone Commands
580
664
 
581
- AsyncFunction("setMicState") {
665
+ SdkAsyncFunction("setMicState") {
582
666
  enabled: Boolean,
583
667
  useGlassesMic: Boolean?,
584
668
  sendTranscript: Boolean?,
@@ -600,7 +684,7 @@ class BluetoothSdkModule : Module() {
600
684
 
601
685
  // MARK: - Audio Playback Monitoring
602
686
 
603
- AsyncFunction("setOwnAppAudioPlaying") { playing: Boolean ->
687
+ SdkAsyncFunction("setOwnAppAudioPlaying") { playing: Boolean ->
604
688
  sdk?.setOwnAppAudioPlaying(playing)
605
689
  }
606
690
 
@@ -619,7 +703,7 @@ class BluetoothSdkModule : Module() {
619
703
 
620
704
  // MARK: - RGB LED Control
621
705
 
622
- AsyncFunction("rgbLedControl") {
706
+ SdkAsyncFunction("rgbLedControl") {
623
707
  requestId: String,
624
708
  packageName: String?,
625
709
  action: String,
@@ -1800,7 +1800,7 @@ class DeviceManager {
1800
1800
  iso = manualIso,
1801
1801
  )
1802
1802
  Bridge.log(
1803
- "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"}"
1803
+ "MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=${routed.requestId} size=${routed.size.value} mode=${routed.mode.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"}"
1804
1804
  )
1805
1805
  val activeSgc = sgc
1806
1806
  if (activeSgc == null) {
@@ -292,6 +292,11 @@ object DeviceStore {
292
292
  "bluetooth" to "camera_fov" -> {
293
293
  DeviceManager.getInstance().sgc?.sendCameraFovSetting()
294
294
  }
295
+ "bluetooth" to "button_video_settings" -> {
296
+ DeviceManager.getInstance().sgc?.sendButtonVideoRecordingSettings()
297
+ }
298
+ // Legacy scalar keys remain supported for older hosts. New code should write the
299
+ // canonical button_video_settings object so width/height/fps update atomically.
295
300
  "bluetooth" to "button_video_width",
296
301
  "bluetooth" to "button_video_height",
297
302
  "bluetooth" to "button_video_fps" -> {
@@ -477,6 +477,12 @@ class MentraBluetoothSdk private constructor(
477
477
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "voice_activity_detection_enabled", enabled)
478
478
  }
479
479
 
480
+ @Deprecated(
481
+ message =
482
+ "Sticky action-button photo presets are deprecated. Prefer per-request " +
483
+ "requestPhoto(...) options (e.g. mode=TEXT for AE ÷3, or explicit per-shot fields). " +
484
+ "This method still works but will be removed in a future release.",
485
+ )
480
486
  fun setPhotoCaptureDefaults(settings: PhotoCaptureDefaults): SettingsAckEvent =
481
487
  performSettingsCommand(
482
488
  setting = "button_photo",
@@ -534,6 +540,12 @@ class MentraBluetoothSdk private constructor(
534
540
  performSettingsCommand(
535
541
  setting = "button_video_recording",
536
542
  updateStore = { _ ->
543
+ DeviceStore.set(
544
+ ObservableStore.BLUETOOTH_CATEGORY,
545
+ "button_video_settings",
546
+ mapOf("width" to defaults.width, "height" to defaults.height, "fps" to defaults.fps),
547
+ )
548
+ // Keep legacy cache keys readable for older internal callers during migration.
537
549
  DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_width", defaults.width)
538
550
  DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_height", defaults.height)
539
551
  DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", defaults.fps)
@@ -167,6 +167,17 @@ data class CameraFovResult(
167
167
  }
168
168
  }
169
169
 
170
+ enum class PhotoMode(val value: String) {
171
+ PHOTO("photo"),
172
+ TEXT("text");
173
+
174
+ companion object {
175
+ @JvmStatic
176
+ fun fromValue(value: String?): PhotoMode =
177
+ values().firstOrNull { it.value == value } ?: PHOTO
178
+ }
179
+ }
180
+
170
181
  data class PhotoRequest @JvmOverloads constructor(
171
182
  val requestId: String = generatedCameraRequestId("photo"),
172
183
  val size: PhotoSize,
@@ -188,6 +199,7 @@ data class PhotoRequest @JvmOverloads constructor(
188
199
  val ispDigitalGain: Int? = null,
189
200
  val ispAnalogGain: String? = null,
190
201
  val resetCaptureTuning: Boolean? = null,
202
+ val mode: PhotoMode = PhotoMode.PHOTO,
191
203
  ) {
192
204
  companion object {
193
205
  /** Mirrors iOS `BluetoothSdkModule` defaults for keys omitted from the JS bridge. */
@@ -225,6 +237,7 @@ data class PhotoRequest @JvmOverloads constructor(
225
237
  compress = PhotoCompression.fromValue(stringValue(values, "compress") ?: "none"),
226
238
  save = boolValue(values, "save", "saveToGallery") ?: false,
227
239
  sound = boolValue(values, "sound") ?: true,
240
+ mode = PhotoMode.fromValue(stringValue(values, "mode")),
228
241
  exposureTimeNs = exposureTimeNs,
229
242
  iso = iso,
230
243
  aeExposureDivisor = aeDivisor,
@@ -2841,7 +2841,7 @@ class G1 : SGCManager() {
2841
2841
  private fun chunkTextForTransmission(text: String?): List<ByteArray> {
2842
2842
  // Handle empty or whitespace-only text by sending at least a space
2843
2843
  // This ensures the display gets updated/cleared properly
2844
- val textToSend = if (text == null || text.trim().isEmpty()) " " else text
2844
+ val textToSend = if (text == null || text.trim().isEmpty()) " " else sanitizeG1DisplayText(text)
2845
2845
  val textBytes = textToSend.toByteArray(StandardCharsets.UTF_8)
2846
2846
  val totalChunks = Math.ceil(textBytes.size.toDouble() / MAX_CHUNK_SIZE).toInt()
2847
2847
 
@@ -0,0 +1,38 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import java.text.Normalizer
4
+
5
+ private val G1_COMBINING_MARKS = Regex("\\p{M}+")
6
+
7
+ /**
8
+ * Converts text to the base Latin glyphs available in the Even Realities G1 firmware.
9
+ *
10
+ * G1 does not ship glyphs for combining diacritics. Normalize at the device boundary so
11
+ * miniapps can retain the real text everywhere else and newer glasses receive it unchanged.
12
+ */
13
+ internal fun sanitizeG1DisplayText(text: String): String {
14
+ val expanded = buildString(text.length) {
15
+ text.forEach { character ->
16
+ when (character) {
17
+ 'Đ', 'Ð' -> append('D')
18
+ 'đ', 'ð' -> append('d')
19
+ 'Ł' -> append('L')
20
+ 'ł' -> append('l')
21
+ 'Ø' -> append('O')
22
+ 'ø' -> append('o')
23
+ 'Æ' -> append("AE")
24
+ 'æ' -> append("ae")
25
+ 'Œ' -> append("OE")
26
+ 'œ' -> append("oe")
27
+ 'ẞ' -> append("SS")
28
+ 'ß' -> append("ss")
29
+ 'Þ' -> append("TH")
30
+ 'þ' -> append("th")
31
+ else -> append(character)
32
+ }
33
+ }
34
+ }
35
+
36
+ return Normalizer.normalize(expanded, Normalizer.Form.NFD)
37
+ .replace(G1_COMBINING_MARKS, "")
38
+ }