@mentra/bluetooth-sdk 0.1.18 → 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 (77) 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 +120 -17
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -6
  5. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -21
  6. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
  7. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +13 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +551 -159
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +361 -67
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +7501 -3704
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +134 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
  17. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -3
  18. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -5
  20. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleWireProtocol.java +108 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +4 -3
  22. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900LengthCodec.java +115 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +109 -73
  24. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +106 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +93 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +111 -11
  27. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
  28. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -9
  29. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +17 -1
  31. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +158 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
  36. package/build/BluetoothSdk.types.d.ts +37 -1
  37. package/build/BluetoothSdk.types.d.ts.map +1 -1
  38. package/build/BluetoothSdk.types.js.map +1 -1
  39. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  40. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  41. package/build/_private/BluetoothSdkModule.js.map +1 -1
  42. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  43. package/build/_private/photoRequestPayload.js +1 -0
  44. package/build/_private/photoRequestPayload.js.map +1 -1
  45. package/build/index.d.ts +1 -1
  46. package/build/index.d.ts.map +1 -1
  47. package/build/index.js +5 -0
  48. package/build/index.js.map +1 -1
  49. package/build/types/index.d.ts +3 -0
  50. package/build/types/index.d.ts.map +1 -0
  51. package/build/types/index.js +2 -0
  52. package/build/types/index.js.map +1 -0
  53. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  54. package/ios/Source/DeviceManager.swift +136 -30
  55. package/ios/Source/DeviceStore.swift +5 -5
  56. package/ios/Source/MentraBluetoothSDK.swift +40 -24
  57. package/ios/Source/OtaManifest.swift +1 -1
  58. package/ios/Source/camera/CameraModels.swift +17 -3
  59. package/ios/Source/sgcs/G1.swift +1 -1
  60. package/ios/Source/sgcs/G2.swift +518 -11
  61. package/ios/Source/sgcs/MentraLive.swift +443 -42
  62. package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
  63. package/ios/Source/sgcs/MentraNex.swift +416 -32
  64. package/ios/Source/sgcs/SGCManager.swift +155 -0
  65. package/ios/Source/sgcs/mentraos_ble.pb.swift +726 -122
  66. package/ios/Source/status/DeviceStatus.swift +0 -8
  67. package/ios/Source/utils/BleJsonCompact.swift +395 -0
  68. package/ios/Source/utils/BleWireProtocol.swift +92 -0
  69. package/ios/Source/utils/G1Text.swift +30 -1
  70. package/ios/Source/utils/MessageChunkReassembler.swift +82 -0
  71. package/ios/Source/utils/MessageChunker.swift +73 -0
  72. package/package.json +14 -7
  73. package/src/BluetoothSdk.types.ts +39 -1
  74. package/src/_private/BluetoothSdkModule.ts +4 -0
  75. package/src/_private/photoRequestPayload.ts +1 -0
  76. package/src/index.ts +6 -0
  77. package/src/types/index.ts +7 -0
@@ -67,6 +67,7 @@ class MentraBluetoothSdk private constructor(
67
67
  private val SCAN_STATE_KEYS = setOf("searching", "searchingController", "searchResults")
68
68
  private const val DEFAULT_SCAN_TIMEOUT_MS = 15_000L
69
69
  private const val DEFAULT_REQUEST_TIMEOUT_MS = 15_000L
70
+ private const val WIFI_SCAN_TIMEOUT_MS = 20_000L
70
71
  private const val VIDEO_UPLOAD_STOP_TIMEOUT_MS = 10 * 60 * 1000L
71
72
  private const val STREAM_START_TIMEOUT_MS = 30_000L
72
73
  private const val STREAM_STOP_TIMEOUT_MS = 15_000L
@@ -476,6 +477,12 @@ class MentraBluetoothSdk private constructor(
476
477
  DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "voice_activity_detection_enabled", enabled)
477
478
  }
478
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
+ )
479
486
  fun setPhotoCaptureDefaults(settings: PhotoCaptureDefaults): SettingsAckEvent =
480
487
  performSettingsCommand(
481
488
  setting = "button_photo",
@@ -533,6 +540,12 @@ class MentraBluetoothSdk private constructor(
533
540
  performSettingsCommand(
534
541
  setting = "button_video_recording",
535
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.
536
549
  DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_width", defaults.width)
537
550
  DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_height", defaults.height)
538
551
  DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", defaults.fps)
@@ -633,35 +646,35 @@ class MentraBluetoothSdk private constructor(
633
646
  fun requestWifiScan(): List<WifiScanResult> {
634
647
  val pending = PendingResponse<List<WifiScanResult>>("WiFi scan request")
635
648
  val request = PendingWifiScan(pending)
636
- synchronized(oneShotLock) {
637
- if (pendingWifiScan != null) {
638
- throw BluetoothSdkException(
639
- "request_in_flight",
640
- "A WiFi scan is already waiting for a glasses response.",
641
- )
649
+ val existing =
650
+ synchronized(oneShotLock) {
651
+ pendingWifiScan ?: run {
652
+ pendingWifiScan = request
653
+ null
654
+ }
642
655
  }
643
- pendingWifiScan = request
656
+ if (existing != null) {
657
+ // Join the in-flight scan instead of failing with request_in_flight;
658
+ // the scan screen auto-starts a scan on mount and can be pushed twice.
659
+ return existing.pending.await(WIFI_SCAN_TIMEOUT_MS)
644
660
  }
645
661
  try {
646
662
  deviceManager.requestWifiScan()
647
- return pending.await()
648
- } catch (error: BluetoothSdkException) {
649
- if (error.code == "request_timeout") {
650
- val fallbackResults =
651
- synchronized(oneShotLock) {
652
- if (request.latestResults.isNotEmpty()) {
653
- if (pendingWifiScan === request) {
654
- pendingWifiScan = null
655
- }
656
- request.latestResults
657
- } else {
658
- emptyList()
659
- }
660
- }
663
+ // The glasses wait up to 15s for scan-results broadcasts before sending
664
+ // scan_complete, so give them longer than that before falling back.
665
+ return pending.await(WIFI_SCAN_TIMEOUT_MS)
666
+ } catch (error: Throwable) {
667
+ if (error is BluetoothSdkException && error.code == "request_timeout") {
668
+ val fallbackResults = synchronized(oneShotLock) { request.latestResults }
661
669
  if (fallbackResults.isNotEmpty()) {
670
+ // Resolve so callers joined on the same scan get the fallback too.
671
+ pending.resolve(fallbackResults)
662
672
  return fallbackResults
663
673
  }
664
674
  }
675
+ // Fail joined callers immediately instead of letting them run out their
676
+ // own timeout.
677
+ pending.reject(error)
665
678
  throw error
666
679
  } finally {
667
680
  synchronized(oneShotLock) {
@@ -57,7 +57,7 @@ internal object OtaManifestChecker {
57
57
  return try {
58
58
  val status = connection.responseCode
59
59
  if (status !in 200..299) {
60
- throw BluetoothSdkException("ota_manifest_request_failed", "OTA manifest request failed with HTTP $status.")
60
+ throw BluetoothSdkException("ota_manifest_request_failed", "OTA manifest request failed with HTTP $status for $otaVersionUrl.")
61
61
  }
62
62
  JSONObject(connection.inputStream.bufferedReader().use { it.readText() })
63
63
  } finally {
@@ -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
+ }