@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.
- package/README.md +52 -3
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +182 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +61 -19
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +32 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +76 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +829 -643
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +34 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +40 -64
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +23 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
- package/build/BluetoothSdk.types.d.ts +45 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +2 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +14 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +24 -7
- package/ios/Source/BluetoothSdkDefaults.swift +27 -0
- package/ios/Source/Bridge.swift +16 -0
- package/ios/Source/DeviceManager.swift +33 -27
- package/ios/Source/DeviceStore.swift +8 -2
- package/ios/Source/MentraBluetoothSDK.swift +160 -8
- package/ios/Source/camera/CameraModels.swift +50 -1
- package/ios/Source/events/BluetoothEvents.swift +3 -0
- package/ios/Source/internal/BluetoothAvailability.swift +20 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
- package/ios/Source/sgcs/G1.swift +3 -3
- package/ios/Source/sgcs/G2.swift +1022 -640
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +31 -7
- package/ios/Source/sgcs/MentraNex.swift +215 -85
- package/ios/Source/sgcs/SGCManager.swift +26 -6
- package/ios/Source/sgcs/Simulated.swift +2 -2
- package/ios/Source/types/DeviceModels.swift +5 -1
- package/package.json +1 -1
- package/plugin/build/index.d.ts +4 -0
- package/plugin/build/withAndroid.d.ts +2 -3
- package/plugin/build/withAndroid.js +46 -0
- package/plugin/build/withIos.d.ts +2 -3
- package/plugin/build/withIos.js +32 -0
- package/src/BluetoothSdk.types.ts +53 -2
- package/src/_private/BluetoothSdkModule.ts +27 -4
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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) {}
|