@mentra/bluetooth-sdk 0.1.10 → 0.1.12

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 (53) hide show
  1. package/README.md +52 -3
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +182 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +61 -19
  5. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +24 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +32 -8
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +76 -5
  9. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -1
  10. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +829 -643
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +34 -5
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +40 -64
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +23 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
  16. package/build/BluetoothSdk.types.d.ts +45 -2
  17. package/build/BluetoothSdk.types.d.ts.map +1 -1
  18. package/build/BluetoothSdk.types.js.map +1 -1
  19. package/build/_private/BluetoothSdkModule.d.ts +2 -1
  20. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  21. package/build/_private/BluetoothSdkModule.js +14 -1
  22. package/build/_private/BluetoothSdkModule.js.map +1 -1
  23. package/build/index.d.ts +1 -1
  24. package/build/index.d.ts.map +1 -1
  25. package/build/index.js +4 -0
  26. package/build/index.js.map +1 -1
  27. package/ios/BluetoothSdkModule.swift +24 -7
  28. package/ios/Source/BluetoothSdkDefaults.swift +27 -0
  29. package/ios/Source/Bridge.swift +16 -0
  30. package/ios/Source/DeviceManager.swift +33 -27
  31. package/ios/Source/DeviceStore.swift +8 -2
  32. package/ios/Source/MentraBluetoothSDK.swift +160 -8
  33. package/ios/Source/camera/CameraModels.swift +50 -1
  34. package/ios/Source/events/BluetoothEvents.swift +3 -0
  35. package/ios/Source/internal/BluetoothAvailability.swift +20 -0
  36. package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
  37. package/ios/Source/sgcs/G1.swift +3 -3
  38. package/ios/Source/sgcs/G2.swift +1022 -640
  39. package/ios/Source/sgcs/Mach1.swift +2 -2
  40. package/ios/Source/sgcs/MentraLive.swift +31 -7
  41. package/ios/Source/sgcs/MentraNex.swift +215 -85
  42. package/ios/Source/sgcs/SGCManager.swift +26 -6
  43. package/ios/Source/sgcs/Simulated.swift +2 -2
  44. package/ios/Source/types/DeviceModels.swift +5 -1
  45. package/package.json +1 -1
  46. package/plugin/build/index.d.ts +4 -0
  47. package/plugin/build/withAndroid.d.ts +2 -3
  48. package/plugin/build/withAndroid.js +46 -0
  49. package/plugin/build/withIos.d.ts +2 -3
  50. package/plugin/build/withIos.js +32 -0
  51. package/src/BluetoothSdk.types.ts +53 -2
  52. package/src/_private/BluetoothSdkModule.ts +27 -4
  53. package/src/index.ts +5 -0
@@ -23,6 +23,7 @@ class MentraBluetoothSdk private constructor(
23
23
  private val deviceManager: DeviceManager
24
24
  private val listeners =
25
25
  Collections.synchronizedSet(mutableSetOf<MentraBluetoothSdkListener>())
26
+ private val analytics = BluetoothSdkAnalytics(appContext, config.analytics)
26
27
  private val discoveredDeviceNames = mutableSetOf<String>()
27
28
  private val bridgeEventSinkId: String
28
29
  private val storeListenerId: String
@@ -50,7 +51,13 @@ class MentraBluetoothSdk private constructor(
50
51
  Bridge.initialize(appContext)
51
52
  deviceManager = DeviceManager.getInstance()
52
53
  bridgeEventSinkId = Bridge.addEventSink { eventName, data -> dispatchBridgeEvent(eventName, data) }
54
+ // Baseline the analytics connection state before subscribing to the store:
55
+ // store updates invoke listeners synchronously on the updating thread, so a
56
+ // connected status observed before the baseline would be reported as a fresh
57
+ // bluetooth_sdk_glasses_connected transition.
58
+ analytics.initializeGlassesStatus(getRawGlassesStatus())
53
59
  storeListenerId = DeviceStore.store.addListener { category, changes -> dispatchStoreUpdate(category, changes) }
60
+ analytics.captureStarted()
54
61
  }
55
62
 
56
63
  companion object {
@@ -58,6 +65,7 @@ class MentraBluetoothSdk private constructor(
58
65
  private val SCAN_STATE_KEYS = setOf("searching", "searchingController", "searchResults")
59
66
  private const val DEFAULT_SCAN_TIMEOUT_MS = 15_000L
60
67
  private const val DEFAULT_REQUEST_TIMEOUT_MS = 15_000L
68
+ private const val VIDEO_UPLOAD_STOP_TIMEOUT_MS = 10 * 60 * 1000L
61
69
  private const val STREAM_START_TIMEOUT_MS = 30_000L
62
70
  private const val STREAM_STOP_TIMEOUT_MS = 15_000L
63
71
  private const val DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS = 5
@@ -97,6 +105,9 @@ class MentraBluetoothSdk private constructor(
97
105
  private data class PendingVideoRecordingRequest(
98
106
  val expectedStatus: String,
99
107
  val pending: PendingResponse<VideoRecordingStatusEvent>,
108
+ val waitForUpload: Boolean = false,
109
+ var stoppedEvent: VideoRecordingStatusEvent? = null,
110
+ var uploadSucceeded: Boolean = false,
100
111
  )
101
112
 
102
113
  private data class PendingWifiScan(
@@ -184,6 +195,15 @@ class MentraBluetoothSdk private constructor(
184
195
 
185
196
  fun getDefaultDevice(): Device? = currentDefaultDevice()
186
197
 
198
+ private fun requireGlassesConnected(operation: String) {
199
+ if (!getRawGlassesStatus().connected) {
200
+ throw BluetoothException(
201
+ "glasses_not_connected",
202
+ "Cannot $operation because glasses are not connected.",
203
+ )
204
+ }
205
+ }
206
+
187
207
  fun setDefaultDevice(device: Device?) {
188
208
  if (device == null) {
189
209
  clearDefaultDevice()
@@ -771,6 +791,7 @@ class MentraBluetoothSdk private constructor(
771
791
 
772
792
  fun startVideoRecording(request: VideoRecordingRequest): VideoRecordingStatusEvent {
773
793
  require(request.requestId.isNotBlank()) { "requestId is required to start video recording." }
794
+ requireGlassesConnected("start video recording")
774
795
  val pending = PendingResponse<VideoRecordingStatusEvent>("start video recording")
775
796
  val pendingRequest = PendingVideoRecordingRequest("recording_started", pending)
776
797
  if (pendingVideoRecordingRequests.putIfAbsent(request.requestId, pendingRequest) != null) {
@@ -787,6 +808,7 @@ class MentraBluetoothSdk private constructor(
787
808
  request.width,
788
809
  request.height,
789
810
  request.fps,
811
+ request.maxRecordingTimeMinutes,
790
812
  )
791
813
  return pending.await()
792
814
  } finally {
@@ -794,10 +816,22 @@ class MentraBluetoothSdk private constructor(
794
816
  }
795
817
  }
796
818
 
797
- fun stopVideoRecording(requestId: String): VideoRecordingStatusEvent {
819
+ @JvmOverloads
820
+ fun stopVideoRecording(
821
+ requestId: String,
822
+ webhookUrl: String? = null,
823
+ authToken: String? = null,
824
+ ): VideoRecordingStatusEvent {
798
825
  require(requestId.isNotBlank()) { "requestId is required to stop video recording." }
826
+ requireGlassesConnected("stop video recording")
799
827
  val pending = PendingResponse<VideoRecordingStatusEvent>("stop video recording")
800
- val pendingRequest = PendingVideoRecordingRequest("recording_stopped", pending)
828
+ val waitForUpload = !webhookUrl.isNullOrBlank()
829
+ val pendingRequest =
830
+ PendingVideoRecordingRequest(
831
+ expectedStatus = "recording_stopped",
832
+ pending = pending,
833
+ waitForUpload = waitForUpload,
834
+ )
801
835
  if (pendingVideoRecordingRequests.putIfAbsent(requestId, pendingRequest) != null) {
802
836
  throw BluetoothException(
803
837
  "request_in_flight",
@@ -805,8 +839,10 @@ class MentraBluetoothSdk private constructor(
805
839
  )
806
840
  }
807
841
  try {
808
- deviceManager.stopVideoRecording(requestId)
809
- return pending.await()
842
+ deviceManager.stopVideoRecording(requestId, webhookUrl, authToken)
843
+ val timeoutMs =
844
+ if (waitForUpload) VIDEO_UPLOAD_STOP_TIMEOUT_MS else DEFAULT_REQUEST_TIMEOUT_MS
845
+ return pending.await(timeoutMs)
810
846
  } finally {
811
847
  pendingVideoRecordingRequests.remove(requestId, pendingRequest)
812
848
  }
@@ -917,12 +953,14 @@ class MentraBluetoothSdk private constructor(
917
953
  stopStreamKeepAliveMonitor()
918
954
  Bridge.removeEventSink(bridgeEventSinkId)
919
955
  DeviceStore.store.removeListener(storeListenerId)
956
+ analytics.shutdown()
920
957
  listeners.clear()
921
958
  }
922
959
 
923
960
  private fun dispatchStoreUpdate(category: String, changes: Map<String, Any>) {
924
961
  when (ObservableStore.normalizeCategory(category)) {
925
962
  "glasses" -> {
963
+ analytics.observeGlassesStatus(getRawGlassesStatus())
926
964
  val state = getState()
927
965
  dispatchToListeners {
928
966
  it.onStateChanged(state)
@@ -1112,6 +1150,11 @@ class MentraBluetoothSdk private constructor(
1112
1150
  handleVideoRecordingStatusForRequests(event)
1113
1151
  dispatchToListeners { it.onVideoRecordingStatus(event) }
1114
1152
  }
1153
+ "media_success", "media_error" -> {
1154
+ val event = MediaUploadEvent(data)
1155
+ handleMediaUploadForRequests(event)
1156
+ dispatchToListeners { it.onMediaUpload(event) }
1157
+ }
1115
1158
  "rgb_led_control_response" -> {
1116
1159
  val event = RgbLedControlResponseEvent(data)
1117
1160
  handleRgbLedResponseForRequests(event)
@@ -1341,7 +1384,14 @@ class MentraBluetoothSdk private constructor(
1341
1384
  val request = pendingVideoRecordingRequests[event.requestId] ?: return
1342
1385
  if (event.success) {
1343
1386
  if (event.status == request.expectedStatus) {
1344
- request.pending.resolve(event)
1387
+ if (request.waitForUpload) {
1388
+ request.stoppedEvent = event
1389
+ if (request.uploadSucceeded) {
1390
+ request.pending.resolve(event)
1391
+ }
1392
+ } else {
1393
+ request.pending.resolve(event)
1394
+ }
1345
1395
  }
1346
1396
  } else {
1347
1397
  request.pending.reject(
@@ -1353,6 +1403,27 @@ class MentraBluetoothSdk private constructor(
1353
1403
  }
1354
1404
  }
1355
1405
 
1406
+ private fun handleMediaUploadForRequests(event: MediaUploadEvent) {
1407
+ if (!event.isVideo) return
1408
+ val request = pendingVideoRecordingRequests[event.requestId] ?: return
1409
+ if (!request.waitForUpload) return
1410
+ if (event.isSuccess) {
1411
+ val stoppedEvent = request.stoppedEvent
1412
+ if (stoppedEvent != null) {
1413
+ request.pending.resolve(stoppedEvent)
1414
+ } else {
1415
+ request.uploadSucceeded = true
1416
+ }
1417
+ } else {
1418
+ request.pending.reject(
1419
+ BluetoothException(
1420
+ "video_upload_failed",
1421
+ event.errorMessage ?: "Video upload failed.",
1422
+ )
1423
+ )
1424
+ }
1425
+ }
1426
+
1356
1427
  private fun handleRgbLedResponseForRequests(event: RgbLedControlResponseEvent) {
1357
1428
  val pending = pendingRgbLedRequests[event.requestId] ?: return
1358
1429
  if (event.state == "success") {
@@ -16,7 +16,8 @@ enum class PhotoSize(val value: String) {
16
16
  enum class ButtonPhotoSize(val value: String) {
17
17
  SMALL("small"),
18
18
  MEDIUM("medium"),
19
- LARGE("large");
19
+ LARGE("large"),
20
+ MAX("max");
20
21
 
21
22
  companion object {
22
23
  @JvmStatic
@@ -226,6 +227,8 @@ data class VideoRecordingRequest(
226
227
  val width: Int = 0,
227
228
  val height: Int = 0,
228
229
  val fps: Int = 0,
230
+ // Optional auto-stop timer in minutes; 0 = record until stopped/interrupted.
231
+ val maxRecordingTimeMinutes: Int = 0,
229
232
  )
230
233
 
231
234
  data class VideoRecordingStatusEvent(
@@ -239,6 +242,19 @@ data class VideoRecordingStatusEvent(
239
242
  val data: Map<String, Any>? get() = stringMapValue(values["data"])
240
243
  }
241
244
 
245
+ data class MediaUploadEvent(
246
+ val values: Map<String, Any>,
247
+ ) {
248
+ val type: String get() = stringValue(values, "type").orEmpty()
249
+ val requestId: String get() = stringValue(values, "requestId").orEmpty()
250
+ val mediaUrl: String? get() = stringValue(values, "mediaUrl")
251
+ val errorMessage: String? get() = stringValue(values, "errorMessage")
252
+ val mediaType: Int? get() = numberValue(values, "mediaType")
253
+ val timestamp: Long get() = longValue(values, "timestamp") ?: System.currentTimeMillis()
254
+ val isSuccess: Boolean get() = type == "media_success"
255
+ val isVideo: Boolean get() = mediaType == 2
256
+ }
257
+
242
258
  data class GalleryStatusEvent(
243
259
  val values: Map<String, Any>,
244
260
  )
@@ -154,6 +154,7 @@ interface MentraBluetoothSdkListener {
154
154
  fun onPhotoResponse(event: PhotoResponseEvent) {}
155
155
  fun onPhotoStatus(event: PhotoStatusEvent) {}
156
156
  fun onVideoRecordingStatus(event: VideoRecordingStatusEvent) {}
157
+ fun onMediaUpload(event: MediaUploadEvent) {}
157
158
  fun onRgbLedControlResponse(event: RgbLedControlResponseEvent) {}
158
159
  fun onStreamStatus(event: StreamStatusEvent) {}
159
160
  fun onKeepAliveAck(event: KeepAliveAckEvent) {}