@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.
Files changed (80) hide show
  1. package/README.md +18 -8
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +48 -48
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +360 -269
  5. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -7
  6. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +152 -72
  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 +25 -11
  9. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +1 -2
  10. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +1 -2
  11. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +51 -8
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -800
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -5
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +423 -117
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +4 -6
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +17 -5
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +3 -7
  20. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
  21. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -33
  22. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +5 -3
  23. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
  24. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +60 -1
  25. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
  26. package/build/BluetoothSdk.types.d.ts +50 -24
  27. package/build/BluetoothSdk.types.d.ts.map +1 -1
  28. package/build/BluetoothSdk.types.js +1 -0
  29. package/build/BluetoothSdk.types.js.map +1 -1
  30. package/build/_internal.d.ts +4 -4
  31. package/build/_internal.js +4 -4
  32. package/build/_internal.js.map +1 -1
  33. package/build/_private/BluetoothSdkModule.d.ts +14 -8
  34. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  35. package/build/_private/BluetoothSdkModule.js +4 -3
  36. package/build/_private/BluetoothSdkModule.js.map +1 -1
  37. package/build/_private/cameraRequestPayload.d.ts +4 -0
  38. package/build/_private/cameraRequestPayload.d.ts.map +1 -0
  39. package/build/_private/cameraRequestPayload.js +25 -0
  40. package/build/_private/cameraRequestPayload.js.map +1 -0
  41. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  42. package/build/_private/photoRequestPayload.js +11 -3
  43. package/build/_private/photoRequestPayload.js.map +1 -1
  44. package/build/index.d.ts +1 -1
  45. package/build/index.d.ts.map +1 -1
  46. package/build/index.js +70 -61
  47. package/build/index.js.map +1 -1
  48. package/ios/BluetoothSdkModule.swift +51 -28
  49. package/ios/Source/Bridge.swift +4 -16
  50. package/ios/Source/DeviceManager.swift +76 -22
  51. package/ios/Source/DeviceStore.swift +0 -4
  52. package/ios/Source/MentraBluetoothSDK.swift +144 -78
  53. package/ios/Source/OtaManifest.swift +13 -13
  54. package/ios/Source/camera/CameraModels.swift +82 -20
  55. package/ios/Source/controllers/ControllerManager.swift +1 -2
  56. package/ios/Source/controllers/R1.swift +1 -2
  57. package/ios/Source/errors/{BluetoothError.swift → BluetoothSdkError.swift} +1 -1
  58. package/ios/Source/events/BluetoothEvents.swift +5 -27
  59. package/ios/Source/internal/BluetoothAvailability.swift +5 -5
  60. package/ios/Source/sgcs/Frame.swift +0 -2
  61. package/ios/Source/sgcs/G1.swift +52 -4
  62. package/ios/Source/sgcs/G2.swift +643 -204
  63. package/ios/Source/sgcs/Mach1.swift +1 -3
  64. package/ios/Source/sgcs/MentraLive.swift +64 -77
  65. package/ios/Source/sgcs/MentraNex.swift +1 -3
  66. package/ios/Source/sgcs/Nimo.swift +1928 -0
  67. package/ios/Source/sgcs/SGCManager.swift +16 -10
  68. package/ios/Source/sgcs/Simulated.swift +2 -6
  69. package/ios/Source/status/DeviceStatus.swift +0 -8
  70. package/ios/Source/streaming/StreamModels.swift +7 -32
  71. package/ios/Source/stt/STTTools.swift +1 -1
  72. package/ios/Source/types/DeviceModels.swift +5 -0
  73. package/ios/Source/utils/Constants.swift +2 -0
  74. package/package.json +6 -1
  75. package/src/BluetoothSdk.types.ts +54 -28
  76. package/src/_internal.ts +4 -4
  77. package/src/_private/BluetoothSdkModule.ts +20 -11
  78. package/src/_private/cameraRequestPayload.ts +29 -0
  79. package/src/_private/photoRequestPayload.ts +12 -3
  80. package/src/index.ts +77 -67
@@ -1,6 +1,13 @@
1
1
  package com.mentra.bluetoothsdk
2
2
 
3
3
  import org.json.JSONObject
4
+ import java.util.UUID
5
+
6
+ internal fun generatedCameraRequestId(prefix: String): String =
7
+ "$prefix-${System.currentTimeMillis()}-${UUID.randomUUID().toString().take(8)}"
8
+
9
+ internal fun nonBlankRequestId(requestId: String?): String? =
10
+ requestId?.trim()?.takeIf { it.isNotEmpty() }
4
11
 
5
12
  enum class PhotoSize(val value: String) {
6
13
  LOW("low"),
@@ -56,8 +63,8 @@ enum class PhotoCompression(val value: String) {
56
63
  }
57
64
  }
58
65
 
59
- data class ButtonPhotoSettings(
60
- val size: ButtonPhotoSize? = null,
66
+ data class PhotoCaptureDefaults(
67
+ val size: PhotoSize? = null,
61
68
  val mfnr: Boolean? = null,
62
69
  val zsl: Boolean? = null,
63
70
  val noiseReduction: Boolean? = null,
@@ -71,7 +78,7 @@ data class ButtonPhotoSettings(
71
78
  val resetCaptureTuning: Boolean = false,
72
79
  )
73
80
 
74
- data class ButtonVideoRecordingSettings(
81
+ data class VideoRecordingDefaults(
75
82
  val width: Int,
76
83
  val height: Int,
77
84
  val fps: Int,
@@ -138,13 +145,13 @@ data class CameraFovResult(
138
145
  fallback: CameraFov,
139
146
  ): CameraFovResult {
140
147
  if (ack.status == "error") {
141
- throw BluetoothException(
148
+ throw BluetoothSdkException(
142
149
  ack.errorCode ?: "camera_fov_failed",
143
150
  ack.errorMessage ?: "Camera FOV request failed.",
144
151
  )
145
152
  }
146
153
  if (!ack.hardwareApplied) {
147
- throw BluetoothException(
154
+ throw BluetoothSdkException(
148
155
  "camera_fov_not_applied",
149
156
  "Camera FOV was saved but not applied to hardware.",
150
157
  )
@@ -161,13 +168,11 @@ data class CameraFovResult(
161
168
  }
162
169
 
163
170
  data class PhotoRequest @JvmOverloads constructor(
164
- val requestId: String,
165
- val appId: String,
171
+ val requestId: String = generatedCameraRequestId("photo"),
166
172
  val size: PhotoSize,
167
173
  val webhookUrl: String,
168
174
  val authToken: String? = null,
169
175
  val compress: PhotoCompression = PhotoCompression.MEDIUM,
170
- val flash: Boolean = true,
171
176
  val save: Boolean = false,
172
177
  val sound: Boolean = true,
173
178
  /** Sensor exposure time for this capture only (ns), or null for auto exposure */
@@ -212,13 +217,12 @@ data class PhotoRequest @JvmOverloads constructor(
212
217
  val ispAnalogGain = stringValue(values, "ispAnalogGain")
213
218
 
214
219
  return PhotoRequest(
215
- requestId = stringValue(values, "requestId", "request_id").orEmpty(),
216
- appId = stringValue(values, "appId", "app_id").orEmpty(),
220
+ requestId = nonBlankRequestId(stringValue(values, "requestId", "request_id"))
221
+ ?: generatedCameraRequestId("photo"),
217
222
  size = PhotoSize.fromValue(stringValue(values, "size") ?: "medium"),
218
223
  webhookUrl = stringValue(values, "webhookUrl", "webhook_url").orEmpty(),
219
224
  authToken = stringValue(values, "authToken", "auth_token")?.takeIf { it.isNotBlank() },
220
225
  compress = PhotoCompression.fromValue(stringValue(values, "compress") ?: "none"),
221
- flash = boolValue(values, "flash") ?: true,
222
226
  save = boolValue(values, "save", "saveToGallery") ?: false,
223
227
  sound = boolValue(values, "sound") ?: true,
224
228
  exposureTimeNs = exposureTimeNs,
@@ -441,3 +445,13 @@ data class PhotoStatusEvent(
441
445
  val errorCode: String? get() = stringValue(values, "errorCode")
442
446
  val errorMessage: String? get() = stringValue(values, "errorMessage")
443
447
  }
448
+
449
+ data class CameraStatusEvent(
450
+ val values: Map<String, Any>,
451
+ ) {
452
+ val requestId: String get() = stringValue(values, "requestId").orEmpty()
453
+ val state: String get() = stringValue(values, "state").orEmpty()
454
+ val timestamp: Long get() = longValue(values, "timestamp") ?: System.currentTimeMillis()
455
+ val errorCode: String? get() = stringValue(values, "errorCode")
456
+ val errorMessage: String? get() = stringValue(values, "errorMessage")
457
+ }
@@ -20,14 +20,13 @@ abstract class ControllerManager {
20
20
  abstract fun startStream(message: Map<String, Any>)
21
21
  abstract fun stopStream()
22
22
  abstract fun sendStreamKeepAlive(message: Map<String, Any>)
23
- abstract fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean)
23
+ abstract fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean)
24
24
  abstract fun stopVideoRecording(requestId: String)
25
25
 
26
26
  // Button Settings
27
27
  abstract fun sendButtonPhotoSettings()
28
28
  abstract fun sendButtonVideoRecordingSettings()
29
29
  abstract fun sendButtonMaxRecordingTime()
30
- abstract fun sendButtonCameraLedSetting()
31
30
 
32
31
  // Display Control
33
32
  abstract fun setBrightness(level: Int, autoMode: Boolean)
@@ -863,7 +863,7 @@ class R1 : ControllerManager() {
863
863
  override fun sortMicRanking(list: MutableList<String>): MutableList<String> = list
864
864
  override fun sendJson(jsonOriginal: Map<String, Any>, wakeUp: Boolean, requireAck: Boolean) {}
865
865
  override fun requestPhoto(request: PhotoRequest) {}
866
- override fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {}
866
+ override fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean) {}
867
867
  override fun stopVideoRecording(requestId: String) {}
868
868
  override fun startStream(message: Map<String, Any>) {}
869
869
  override fun stopStream() {}
@@ -871,7 +871,6 @@ class R1 : ControllerManager() {
871
871
  override fun sendButtonPhotoSettings() {}
872
872
  override fun sendButtonVideoRecordingSettings() {}
873
873
  override fun sendButtonMaxRecordingTime() {}
874
- override fun sendButtonCameraLedSetting() {}
875
874
  override fun setBrightness(level: Int, autoMode: Boolean) {}
876
875
  override fun clearDisplay() {}
877
876
  override fun sendTextWall(text: String) {}
@@ -39,27 +39,6 @@ data class SpeakingStatusEvent(
39
39
  val values: Map<String, Any>,
40
40
  )
41
41
 
42
- data class OtaUpdateAvailableEvent(
43
- val versionCode: Long?,
44
- val versionName: String?,
45
- val updates: List<String>,
46
- val totalSize: Long?,
47
- val cacheReady: Boolean?,
48
- val values: Map<String, Any>,
49
- ) {
50
- companion object {
51
- internal fun fromMap(values: Map<String, Any>): OtaUpdateAvailableEvent =
52
- OtaUpdateAvailableEvent(
53
- versionCode = longValue(values, "version_code"),
54
- versionName = stringValue(values, "version_name"),
55
- updates = (values["updates"] as? List<*>)?.filterIsInstance<String>() ?: emptyList(),
56
- totalSize = longValue(values, "total_size"),
57
- cacheReady = boolValue(values, "cache_ready"),
58
- values = values,
59
- )
60
- }
61
- }
62
-
63
42
  data class OtaStartAckEvent(
64
43
  val timestamp: Long?,
65
44
  val values: Map<String, Any>,
@@ -153,12 +132,12 @@ interface MentraBluetoothSdkListener {
153
132
  fun onGalleryStatus(event: GalleryStatusEvent) {}
154
133
  fun onPhotoResponse(event: PhotoResponseEvent) {}
155
134
  fun onPhotoStatus(event: PhotoStatusEvent) {}
135
+ fun onCameraStatus(event: CameraStatusEvent) {}
156
136
  fun onVideoRecordingStatus(event: VideoRecordingStatusEvent) {}
157
137
  fun onMediaUpload(event: MediaUploadEvent) {}
158
138
  fun onRgbLedControlResponse(event: RgbLedControlResponseEvent) {}
159
139
  fun onStreamStatus(event: StreamStatusEvent) {}
160
140
  fun onKeepAliveAck(event: KeepAliveAckEvent) {}
161
- fun onOtaUpdateAvailable(event: OtaUpdateAvailableEvent) {}
162
141
  fun onOtaStartAck(event: OtaStartAckEvent) {}
163
142
  fun onOtaStatus(event: OtaStatusEvent) {}
164
143
  fun onSettingsAck(event: SettingsAckEvent) {}
@@ -1584,7 +1584,7 @@ class G1 : SGCManager() {
1584
1584
 
1585
1585
  }
1586
1586
 
1587
- override fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {
1587
+ override fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean) {
1588
1588
 
1589
1589
  }
1590
1590
 
@@ -1600,10 +1600,6 @@ class G1 : SGCManager() {
1600
1600
 
1601
1601
  }
1602
1602
 
1603
- override fun sendButtonCameraLedSetting() {
1604
-
1605
- }
1606
-
1607
1603
  override fun sendCameraFovSetting() {
1608
1604
  }
1609
1605
 
@@ -1618,25 +1614,72 @@ class G1 : SGCManager() {
1618
1614
 
1619
1615
  override fun clearDisplay() {
1620
1616
  Bridge.log("G1: clearDisplay() - sending space")
1621
- sendTextWall(" ")
1617
+ // Bypass the throttle (a clear must always land) and drop any pending caption so it can't
1618
+ // overwrite the clear after the fact.
1619
+ cancelPendingThrottledText()
1620
+ displayTextWall(" ")
1621
+ }
1622
+
1623
+ // G1-specific display throttle (300ms, last-wins) — see G1.swift. G1 firmware can't absorb
1624
+ // rapid text-wall updates; coalesce to the latest within a 300ms window. Belongs in the G1 SGC
1625
+ // (G1 hardware quirk); G2 deliberately does NOT throttle (it must show every caption). The
1626
+ // trailing flush always sends the most recent text, so the final caption is never dropped —
1627
+ // only intermediate frames within a window are coalesced.
1628
+ private val textThrottleHandler = Handler(Looper.getMainLooper())
1629
+ private var textThrottlePending: String? = null
1630
+ private var textThrottleLastSent: Long = 0L
1631
+ private var textThrottleScheduled = false
1632
+ private val TEXT_THROTTLE_WINDOW_MS = 300L
1633
+
1634
+ /** Drop any pending throttled text-wall flush so it can't later overwrite a newer, non-text
1635
+ * display write (a clear, double-text-wall, or bitmap). The posted flush no-ops when
1636
+ * `textThrottlePending` is null. Called from every G1 display path that bypasses the throttle. */
1637
+ private fun cancelPendingThrottledText() {
1638
+ textThrottlePending = null
1639
+ }
1640
+
1641
+ private fun throttledTextWall(text: String) {
1642
+ val now = android.os.SystemClock.uptimeMillis()
1643
+ val sinceLast = now - textThrottleLastSent
1644
+ if (sinceLast >= TEXT_THROTTLE_WINDOW_MS) {
1645
+ // Past the window — send now.
1646
+ textThrottleLastSent = now
1647
+ textThrottlePending = null
1648
+ displayTextWall(text)
1649
+ } else {
1650
+ // Inside the window — keep only the latest and schedule one trailing flush.
1651
+ textThrottlePending = text
1652
+ if (!textThrottleScheduled) {
1653
+ textThrottleScheduled = true
1654
+ textThrottleHandler.postDelayed({
1655
+ textThrottleScheduled = false
1656
+ val pending = textThrottlePending ?: return@postDelayed
1657
+ textThrottlePending = null
1658
+ textThrottleLastSent = android.os.SystemClock.uptimeMillis()
1659
+ displayTextWall(pending)
1660
+ }, TEXT_THROTTLE_WINDOW_MS - sinceLast)
1661
+ }
1662
+ }
1622
1663
  }
1623
1664
 
1624
1665
  override fun sendText(text: String) {
1625
1666
  Bridge.log("G1: sendText() - text: " + text)
1626
- displayTextWall(text)
1667
+ throttledTextWall(text)
1627
1668
  }
1628
1669
 
1629
1670
  override fun sendTextWall(text: String) {
1630
1671
  // Bridge.log("G1: sendTextWall() - text: " + text);
1631
- displayTextWall(text)
1672
+ throttledTextWall(text)
1632
1673
  }
1633
1674
 
1634
1675
  override fun sendDoubleTextWall(top: String, bottom: String) {
1676
+ cancelPendingThrottledText() // a newer layout supersedes any pending caption text
1635
1677
  Bridge.log("G1: sendDoubleTextWall() - top: " + top + ", bottom: " + bottom)
1636
1678
  displayDoubleTextWall(top, bottom)
1637
1679
  }
1638
1680
 
1639
1681
  override fun displayBitmap(base64ImageData: String, x: Int?, y: Int?, width: Int?, height: Int?): Boolean {
1682
+ cancelPendingThrottledText() // a bitmap supersedes any pending caption text
1640
1683
  try {
1641
1684
  // Decode base64 to byte array
1642
1685
  val bmpData = android.util.Base64.decode(base64ImageData, android.util.Base64.DEFAULT)