@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
@@ -20,7 +20,7 @@ abstract class SGCManager {
20
20
  abstract fun startStream(message: MutableMap<String, Any>)
21
21
  abstract fun stopStream()
22
22
  abstract fun sendStreamKeepAlive(message: MutableMap<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
 
25
25
  /**
26
26
  * Start video recording with optional per-recording resolution/fps. A width,
@@ -32,14 +32,13 @@ abstract class SGCManager {
32
32
  open fun startVideoRecording(
33
33
  requestId: String,
34
34
  save: Boolean,
35
- flash: Boolean,
36
35
  sound: Boolean,
37
36
  width: Int,
38
37
  height: Int,
39
38
  fps: Int,
40
39
  maxRecordingTimeMinutes: Int,
41
40
  ) {
42
- startVideoRecording(requestId, save, flash, sound)
41
+ startVideoRecording(requestId, save, sound)
43
42
  }
44
43
 
45
44
  abstract fun stopVideoRecording(requestId: String)
@@ -59,7 +58,6 @@ abstract class SGCManager {
59
58
  abstract fun sendButtonPhotoSettings()
60
59
  abstract fun sendButtonVideoRecordingSettings()
61
60
  abstract fun sendButtonMaxRecordingTime()
62
- abstract fun sendButtonCameraLedSetting()
63
61
  abstract fun sendCameraFovSetting()
64
62
 
65
63
  // Display Control
@@ -79,6 +77,20 @@ abstract class SGCManager {
79
77
  width: Int? = null,
80
78
  height: Int? = null
81
79
  ): Boolean
80
+
81
+ /**
82
+ * Show text in a positioned container with an optional rounded border.
83
+ * G2-only capability; default no-op so other glasses ignore it.
84
+ */
85
+ open fun sendPositionedText(
86
+ text: String,
87
+ x: Int,
88
+ y: Int,
89
+ width: Int,
90
+ height: Int,
91
+ borderWidth: Int = 0,
92
+ borderRadius: Int = 0
93
+ ) {}
82
94
  abstract fun showDashboard()
83
95
  abstract fun setDashboardPosition(height: Int, depth: Int)
84
96
 
@@ -173,7 +185,7 @@ abstract class SGCManager {
173
185
  // Voice Activity Detection
174
186
  open fun sendVoiceActivityDetectionSetting() {}
175
187
 
176
- // Start/stop LC3 audio playback from glasses based on the nex_audio_playback flag.
188
+ // Start/stop LC3 audio playback from glasses based on the nex_lc3_audio_playback flag.
177
189
  open fun applyNexAudioPlaybackSetting() {}
178
190
 
179
191
  // Version info
@@ -34,7 +34,7 @@ class Simulated : SGCManager() {
34
34
 
35
35
  // Camera & Media
36
36
  override fun requestPhoto(request: PhotoRequest) {
37
- Bridge.log("requestPhoto flash=${request.flash}, save=${request.save}, sound=${request.sound}")
37
+ Bridge.log("requestPhoto save=${request.save}, sound=${request.sound}")
38
38
  }
39
39
 
40
40
  override fun startStream(message: MutableMap<String, Any>) {
@@ -49,8 +49,8 @@ class Simulated : SGCManager() {
49
49
  Bridge.log("sendStreamKeepAlive")
50
50
  }
51
51
 
52
- override fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {
53
- Bridge.log("startVideoRecording flash=$flash, sound=$sound")
52
+ override fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean) {
53
+ Bridge.log("startVideoRecording sound=$sound")
54
54
  }
55
55
 
56
56
  override fun stopVideoRecording(requestId: String) {
@@ -70,10 +70,6 @@ class Simulated : SGCManager() {
70
70
  Bridge.log("sendButtonMaxRecordingTime")
71
71
  }
72
72
 
73
- override fun sendButtonCameraLedSetting() {
74
- Bridge.log("sendButtonCameraLedSetting")
75
- }
76
-
77
73
  override fun sendCameraFovSetting() {
78
74
  Bridge.log("sendCameraFovSetting")
79
75
  }
@@ -248,7 +248,6 @@ internal data class BluetoothStatus(
248
248
  val contextualDashboard: Boolean,
249
249
  val galleryModeEnabled: Boolean,
250
250
  val buttonPhotoSize: ButtonPhotoSize,
251
- val buttonCameraLed: Boolean,
252
251
  val buttonMaxRecordingTime: Int,
253
252
  val buttonVideoWidth: Int,
254
253
  val buttonVideoHeight: Int,
@@ -301,7 +300,6 @@ internal data class BluetoothStatus(
301
300
  "contextual_dashboard" to contextualDashboard,
302
301
  "galleryModeEnabled" to galleryModeEnabled,
303
302
  "button_photo_size" to buttonPhotoSize.value,
304
- "button_camera_led" to buttonCameraLed,
305
303
  "button_max_recording_time" to buttonMaxRecordingTime,
306
304
  "button_video_width" to buttonVideoWidth,
307
305
  "button_video_height" to buttonVideoHeight,
@@ -349,7 +347,6 @@ internal data class BluetoothStatus(
349
347
  galleryModeEnabled =
350
348
  boolValue(values, "gallery_mode") ?: boolValue(values, "galleryModeEnabled") ?: true,
351
349
  buttonPhotoSize = ButtonPhotoSize.fromValue(stringValue(values, "button_photo_size")),
352
- buttonCameraLed = boolValue(values, "button_camera_led") ?: true,
353
350
  buttonMaxRecordingTime = numberValue(values, "button_max_recording_time") ?: 10,
354
351
  buttonVideoWidth = numberValue(values, "button_video_width") ?: 1280,
355
352
  buttonVideoHeight = numberValue(values, "button_video_height") ?: 720,
@@ -554,7 +551,6 @@ internal data class BluetoothStatusUpdate(
554
551
  val contextualDashboard: Boolean? = null,
555
552
  val galleryModeEnabled: Boolean? = null,
556
553
  val buttonPhotoSize: ButtonPhotoSize? = null,
557
- val buttonCameraLed: Boolean? = null,
558
554
  val buttonMaxRecordingTime: Int? = null,
559
555
  val buttonVideoWidth: Int? = null,
560
556
  val buttonVideoHeight: Int? = null,
@@ -597,7 +593,6 @@ internal data class BluetoothStatusUpdate(
597
593
  putIfNotNull("contextual_dashboard", contextualDashboard)
598
594
  putIfNotNull("galleryModeEnabled", galleryModeEnabled)
599
595
  buttonPhotoSize?.let { put("button_photo_size", it.value) }
600
- putIfNotNull("button_camera_led", buttonCameraLed)
601
596
  putIfNotNull("button_max_recording_time", buttonMaxRecordingTime)
602
597
  putIfNotNull("button_video_width", buttonVideoWidth)
603
598
  putIfNotNull("button_video_height", buttonVideoHeight)
@@ -648,7 +643,6 @@ internal data class BluetoothStatusUpdate(
648
643
  optionalBoolValue(values, "gallery_mode") ?: optionalBoolValue(values, "galleryModeEnabled"),
649
644
  buttonPhotoSize =
650
645
  optionalStringValue(values, "button_photo_size")?.let(ButtonPhotoSize::fromValue),
651
- buttonCameraLed = optionalBoolValue(values, "button_camera_led"),
652
646
  buttonMaxRecordingTime = optionalNumberValue(values, "button_max_recording_time"),
653
647
  buttonVideoWidth = optionalNumberValue(values, "button_video_width"),
654
648
  buttonVideoHeight = optionalNumberValue(values, "button_video_height"),
@@ -11,6 +11,7 @@ data class StreamVideoConfig @JvmOverloads constructor(
11
11
  width?.let { "width" to it },
12
12
  height?.let { "height" to it },
13
13
  bitrate?.let { "bitrate" to it },
14
+ // ASG stream parsers shipped with the BLE key named "frameRate".
14
15
  fps?.let { "frameRate" to it },
15
16
  ).toMap()
16
17
 
@@ -170,28 +171,19 @@ data class StreamResolvedConfig @JvmOverloads constructor(
170
171
  data class StreamRequest @JvmOverloads constructor(
171
172
  val streamUrl: String,
172
173
  val streamId: String = "",
173
- val keepAlive: Boolean = true,
174
- val keepAliveIntervalSeconds: Int = 5,
175
174
  val sound: Boolean = true,
176
175
  val video: StreamVideoConfig? = null,
177
176
  val audio: StreamAudioConfig? = null,
178
- val extraValues: Map<String, Any> = emptyMap(),
179
177
  ) {
180
- fun toMap(): Map<String, Any> {
181
- val values = extraValues.toMutableMap()
182
- values.remove("keepAliveMode")
183
- values["type"] = "start_stream"
184
- values["streamUrl"] = streamUrl
185
- values["streamId"] = streamId
186
- values["keepAlive"] = keepAlive
187
- values["keepAliveIntervalSeconds"] = keepAliveIntervalSeconds
188
- // The camera light is a privacy indicator and cannot be disabled by SDK callers.
189
- values["flash"] = true
190
- values["sound"] = sound
191
- video?.toMap()?.takeIf { it.isNotEmpty() }?.let { values["video"] = it }
192
- audio?.toMap()?.takeIf { it.isNotEmpty() }?.let { values["audio"] = it }
193
- return values
194
- }
178
+ fun toMap(): Map<String, Any> =
179
+ buildMap {
180
+ put("type", "start_stream")
181
+ put("streamUrl", streamUrl)
182
+ put("streamId", streamId)
183
+ put("sound", sound)
184
+ video?.toMap()?.takeIf { it.isNotEmpty() }?.let { put("video", it) }
185
+ audio?.toMap()?.takeIf { it.isNotEmpty() }?.let { put("audio", it) }
186
+ }
195
187
 
196
188
  companion object {
197
189
  @JvmStatic
@@ -201,31 +193,23 @@ data class StreamRequest @JvmOverloads constructor(
201
193
  (values["streamUrl"] ?: values["rtmpUrl"] ?: values["srtUrl"] ?: values["whipUrl"]) as? String
202
194
  ?: "",
203
195
  streamId = values["streamId"] as? String ?: "",
204
- keepAlive = values["keepAlive"] as? Boolean ?: true,
205
- keepAliveIntervalSeconds = (values["keepAliveIntervalSeconds"] as? Number)?.toInt() ?: 5,
206
196
  sound = values["sound"] as? Boolean ?: true,
207
197
  video = StreamVideoConfig.fromMap(stringMapValue(values["video"])),
208
198
  audio = StreamAudioConfig.fromMap(stringMapValue(values["audio"])),
209
- extraValues = values,
210
199
  )
211
200
  }
212
201
  }
213
202
 
214
- internal fun StreamRequest.isExternallyManagedKeepAlive(): Boolean =
215
- stringValue(extraValues, "keepAliveMode") == "external"
216
-
217
203
  internal data class StreamKeepAliveRequest @JvmOverloads constructor(
218
204
  val streamId: String,
219
205
  val ackId: String,
220
- val extraValues: Map<String, Any> = emptyMap(),
221
206
  ) {
222
- fun toMap(): Map<String, Any> {
223
- val values = extraValues.toMutableMap()
224
- values["type"] = "keep_stream_alive"
225
- values["streamId"] = streamId
226
- values["ackId"] = ackId
227
- return values
228
- }
207
+ fun toMap(): Map<String, Any> =
208
+ mapOf(
209
+ "type" to "keep_stream_alive",
210
+ "streamId" to streamId,
211
+ "ackId" to ackId,
212
+ )
229
213
 
230
214
  companion object {
231
215
  @JvmStatic
@@ -233,7 +217,6 @@ internal data class StreamKeepAliveRequest @JvmOverloads constructor(
233
217
  StreamKeepAliveRequest(
234
218
  streamId = values["streamId"] as? String ?: "",
235
219
  ackId = values["ackId"] as? String ?: "",
236
- extraValues = values,
237
220
  )
238
221
  }
239
222
  }
@@ -2,17 +2,18 @@ package com.mentra.bluetoothsdk
2
2
 
3
3
  import com.mentra.bluetoothsdk.utils.ControllerTypes
4
4
  import com.mentra.bluetoothsdk.utils.DeviceTypes
5
+ import expo.modules.kotlin.exception.CodedException
5
6
 
6
7
  data class MentraBluetoothSdkConfig @JvmOverloads constructor(
7
8
  val deliverCallbacksOnMainThread: Boolean = true,
8
9
  val analytics: BluetoothSdkAnalyticsConfig = BluetoothSdkAnalyticsConfig(),
9
10
  )
10
11
 
11
- class BluetoothException(
12
- val code: String,
12
+ class BluetoothSdkException(
13
+ code: String,
13
14
  message: String,
14
15
  cause: Throwable? = null,
15
- ) : IllegalStateException(message, cause)
16
+ ) : CodedException(code, message, cause)
16
17
 
17
18
  enum class DeviceModel(val deviceType: String) {
18
19
  G1(DeviceTypes.G1),
@@ -22,6 +23,7 @@ enum class DeviceModel(val deviceType: String) {
22
23
  MACH1(DeviceTypes.MACH1),
23
24
  Z100(DeviceTypes.Z100),
24
25
  FRAME(DeviceTypes.FRAME),
26
+ NIMO(DeviceTypes.NIMO),
25
27
  SIMULATED(DeviceTypes.SIMULATED),
26
28
  R1(ControllerTypes.R1);
27
29
 
@@ -9,7 +9,8 @@ object DeviceTypes {
9
9
  const val Z100 = "Vuzix Z100"
10
10
  const val FRAME = "Brilliant Frame"
11
11
  const val G2 = "Even Realities G2"
12
- val ALL = arrayOf(SIMULATED, G1, G2, MACH1, LIVE, Z100, FRAME, NEX)
12
+ const val NIMO = "NIMO"
13
+ val ALL = arrayOf(SIMULATED, G1, G2, MACH1, LIVE, Z100, FRAME, NEX, NIMO)
13
14
  }
14
15
 
15
16
  object ControllerTypes {
@@ -1,16 +1,33 @@
1
1
  package com.mentra.bluetoothsdk.camera
2
2
 
3
+ import com.mentra.bluetoothsdk.PhotoCompression
4
+ import com.mentra.bluetoothsdk.PhotoRequest
5
+ import com.mentra.bluetoothsdk.PhotoSize
3
6
  import kotlin.test.Test
7
+ import kotlin.test.assertEquals
4
8
  import kotlin.test.assertNull
9
+ import kotlin.test.assertTrue
5
10
 
6
11
  class PhotoRequestTest {
12
+ @Test
13
+ fun `constructor generates requestId when omitted`() {
14
+ val request =
15
+ PhotoRequest(
16
+ size = PhotoSize.MEDIUM,
17
+ webhookUrl = "https://example.com/upload",
18
+ compress = PhotoCompression.NONE,
19
+ sound = true,
20
+ )
21
+
22
+ assertTrue(request.requestId.startsWith("photo-"))
23
+ }
24
+
7
25
  @Test
8
26
  fun `fromMap defaults exposureTimeNs null`() {
9
27
  val request =
10
28
  PhotoRequest.fromMap(
11
29
  mapOf(
12
30
  "requestId" to "photo-1",
13
- "appId" to "com.test.app",
14
31
  "size" to "medium",
15
32
  "webhookUrl" to "https://example.com/upload",
16
33
  "compress" to "none",
@@ -20,4 +37,46 @@ class PhotoRequestTest {
20
37
 
21
38
  assertNull(request.exposureTimeNs)
22
39
  }
40
+
41
+ @Test
42
+ fun `fromMap generates requestId when omitted or blank`() {
43
+ val withoutRequestId =
44
+ PhotoRequest.fromMap(
45
+ mapOf(
46
+ "size" to "medium",
47
+ "webhookUrl" to "https://example.com/upload",
48
+ "compress" to "none",
49
+ "sound" to true,
50
+ )
51
+ )
52
+ val blankRequestId =
53
+ PhotoRequest.fromMap(
54
+ mapOf(
55
+ "requestId" to " ",
56
+ "size" to "medium",
57
+ "webhookUrl" to "https://example.com/upload",
58
+ "compress" to "none",
59
+ "sound" to true,
60
+ )
61
+ )
62
+
63
+ assertTrue(withoutRequestId.requestId.startsWith("photo-"))
64
+ assertTrue(blankRequestId.requestId.startsWith("photo-"))
65
+ }
66
+
67
+ @Test
68
+ fun `fromMap preserves explicit requestId`() {
69
+ val request =
70
+ PhotoRequest.fromMap(
71
+ mapOf(
72
+ "requestId" to "photo-1",
73
+ "size" to "medium",
74
+ "webhookUrl" to "https://example.com/upload",
75
+ "compress" to "none",
76
+ "sound" to true,
77
+ )
78
+ )
79
+
80
+ assertEquals("photo-1", request.requestId)
81
+ }
23
82
  }