@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.
- package/README.md +18 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +48 -48
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +360 -269
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +152 -72
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +12 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +25 -11
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +1 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +1 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +51 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -800
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +423 -117
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +4 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +17 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +3 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -33
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +5 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +60 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
- package/build/BluetoothSdk.types.d.ts +50 -24
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +4 -4
- package/build/_internal.js +4 -4
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +14 -8
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +4 -3
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/cameraRequestPayload.d.ts +4 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -0
- package/build/_private/cameraRequestPayload.js +25 -0
- package/build/_private/cameraRequestPayload.js.map +1 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +11 -3
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +70 -61
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +51 -28
- package/ios/Source/Bridge.swift +4 -16
- package/ios/Source/DeviceManager.swift +76 -22
- package/ios/Source/DeviceStore.swift +0 -4
- package/ios/Source/MentraBluetoothSDK.swift +144 -78
- package/ios/Source/OtaManifest.swift +13 -13
- package/ios/Source/camera/CameraModels.swift +82 -20
- package/ios/Source/controllers/ControllerManager.swift +1 -2
- package/ios/Source/controllers/R1.swift +1 -2
- package/ios/Source/errors/{BluetoothError.swift → BluetoothSdkError.swift} +1 -1
- package/ios/Source/events/BluetoothEvents.swift +5 -27
- package/ios/Source/internal/BluetoothAvailability.swift +5 -5
- package/ios/Source/sgcs/Frame.swift +0 -2
- package/ios/Source/sgcs/G1.swift +52 -4
- package/ios/Source/sgcs/G2.swift +643 -204
- package/ios/Source/sgcs/Mach1.swift +1 -3
- package/ios/Source/sgcs/MentraLive.swift +64 -77
- package/ios/Source/sgcs/MentraNex.swift +1 -3
- package/ios/Source/sgcs/Nimo.swift +1928 -0
- package/ios/Source/sgcs/SGCManager.swift +16 -10
- package/ios/Source/sgcs/Simulated.swift +2 -6
- package/ios/Source/status/DeviceStatus.swift +0 -8
- package/ios/Source/streaming/StreamModels.swift +7 -32
- package/ios/Source/stt/STTTools.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +5 -0
- package/ios/Source/utils/Constants.swift +2 -0
- package/package.json +6 -1
- package/src/BluetoothSdk.types.ts +54 -28
- package/src/_internal.ts +4 -4
- package/src/_private/BluetoothSdkModule.ts +20 -11
- package/src/_private/cameraRequestPayload.ts +29 -0
- package/src/_private/photoRequestPayload.ts +12 -3
- package/src/index.ts +77 -67
|
@@ -127,7 +127,6 @@ object DeviceStore {
|
|
|
127
127
|
store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
128
128
|
store.set("bluetooth", "screen_disabled", false)
|
|
129
129
|
store.set("bluetooth", "button_photo_size", "max")
|
|
130
|
-
store.set("bluetooth", "button_camera_led", true)
|
|
131
130
|
store.set("bluetooth", "button_max_recording_time", 10)
|
|
132
131
|
store.set("bluetooth", "camera_fov", mapOf("fov" to 118, "roi_position" to 0))
|
|
133
132
|
store.set("bluetooth", "button_video_width", 1280)
|
|
@@ -143,7 +142,7 @@ object DeviceStore {
|
|
|
143
142
|
store.set("bluetooth", "use_native_dashboard", false)
|
|
144
143
|
// Mentra Nex feature flags (off by default; toggled from Nex Developer Settings):
|
|
145
144
|
store.set("bluetooth", "nex_chinese_captions", false)
|
|
146
|
-
store.set("bluetooth", "
|
|
145
|
+
store.set("bluetooth", "nex_lc3_audio_playback", false)
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
fun get(category: String, key: String): Any? {
|
|
@@ -269,9 +268,9 @@ object DeviceStore {
|
|
|
269
268
|
"bluetooth" to "voice_activity_detection_enabled" -> {
|
|
270
269
|
DeviceManager.getInstance().sgc?.sendVoiceActivityDetectionSetting()
|
|
271
270
|
}
|
|
272
|
-
"bluetooth" to "
|
|
271
|
+
"bluetooth" to "nex_lc3_audio_playback" -> {
|
|
273
272
|
(value as? Boolean)?.let { enabled ->
|
|
274
|
-
Bridge.log("DeviceStore:
|
|
273
|
+
Bridge.log("DeviceStore: nex_lc3_audio_playback changed to $enabled")
|
|
275
274
|
DeviceManager.getInstance().sgc?.applyNexAudioPlaybackSetting()
|
|
276
275
|
}
|
|
277
276
|
}
|
|
@@ -287,9 +286,6 @@ object DeviceStore {
|
|
|
287
286
|
"bluetooth" to "button_photo_size" -> {
|
|
288
287
|
DeviceManager.getInstance().sgc?.sendButtonPhotoSettings()
|
|
289
288
|
}
|
|
290
|
-
"bluetooth" to "button_camera_led" -> {
|
|
291
|
-
DeviceManager.getInstance().sgc?.sendButtonCameraLedSetting()
|
|
292
|
-
}
|
|
293
289
|
"bluetooth" to "button_max_recording_time" -> {
|
|
294
290
|
DeviceManager.getInstance().sgc?.sendButtonMaxRecordingTime()
|
|
295
291
|
}
|
|
@@ -31,6 +31,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
31
31
|
private val streamKeepAliveLock = Any()
|
|
32
32
|
private var activeStreamKeepAlive: ActiveStreamKeepAlive? = null
|
|
33
33
|
private val pendingPhotoRequests = ConcurrentHashMap<String, PendingResponse<PhotoResponseEvent>>()
|
|
34
|
+
private val pendingCameraStatusRequests = ConcurrentHashMap<String, PendingResponse<CameraStatusEvent>>()
|
|
34
35
|
private val pendingVideoRecordingRequests =
|
|
35
36
|
ConcurrentHashMap<String, PendingVideoRecordingRequest>()
|
|
36
37
|
private val pendingRgbLedRequests = ConcurrentHashMap<String, PendingResponse<RgbLedControlResponseEvent>>()
|
|
@@ -116,6 +117,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
116
117
|
|
|
117
118
|
private data class PendingWifiScan(
|
|
118
119
|
val pending: PendingResponse<List<WifiScanResult>>,
|
|
120
|
+
var latestResults: List<WifiScanResult> = emptyList(),
|
|
119
121
|
)
|
|
120
122
|
|
|
121
123
|
private data class PendingWifiStatusRequest(
|
|
@@ -158,13 +160,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
158
160
|
|
|
159
161
|
fun await(timeoutMs: Long = DEFAULT_REQUEST_TIMEOUT_MS): T {
|
|
160
162
|
if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
|
|
161
|
-
throw
|
|
163
|
+
throw BluetoothSdkException(
|
|
162
164
|
"request_timeout",
|
|
163
165
|
"$operation timed out waiting for glasses response.",
|
|
164
166
|
)
|
|
165
167
|
}
|
|
166
168
|
error?.let { throw it }
|
|
167
|
-
return result ?: throw
|
|
169
|
+
return result ?: throw BluetoothSdkException(
|
|
168
170
|
"empty_response",
|
|
169
171
|
"$operation completed without a response payload.",
|
|
170
172
|
)
|
|
@@ -201,7 +203,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
201
203
|
|
|
202
204
|
private fun requireGlassesConnected(operation: String) {
|
|
203
205
|
if (!getRawGlassesStatus().connected) {
|
|
204
|
-
throw
|
|
206
|
+
throw BluetoothSdkException(
|
|
205
207
|
"glasses_not_connected",
|
|
206
208
|
"Cannot $operation because glasses are not connected.",
|
|
207
209
|
)
|
|
@@ -353,7 +355,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
353
355
|
fun connectDefault(options: ConnectOptions = ConnectOptions()) {
|
|
354
356
|
val defaultDevice =
|
|
355
357
|
currentDefaultDevice()
|
|
356
|
-
?: throw
|
|
358
|
+
?: throw BluetoothSdkException(
|
|
357
359
|
"default_device_missing",
|
|
358
360
|
"Set a default glasses device before calling connectDefault.",
|
|
359
361
|
)
|
|
@@ -474,7 +476,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
474
476
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "voice_activity_detection_enabled", enabled)
|
|
475
477
|
}
|
|
476
478
|
|
|
477
|
-
fun
|
|
479
|
+
fun setPhotoCaptureDefaults(settings: PhotoCaptureDefaults): SettingsAckEvent =
|
|
478
480
|
performSettingsCommand(
|
|
479
481
|
setting = "button_photo",
|
|
480
482
|
updateStore = { _ ->
|
|
@@ -527,30 +529,20 @@ class MentraBluetoothSdk private constructor(
|
|
|
527
529
|
send = { requestId -> deviceManager.sendButtonPhotoSettings(requestId, settings) },
|
|
528
530
|
)
|
|
529
531
|
|
|
530
|
-
fun
|
|
532
|
+
fun setVideoRecordingDefaults(defaults: VideoRecordingDefaults): SettingsAckEvent =
|
|
531
533
|
performSettingsCommand(
|
|
532
534
|
setting = "button_video_recording",
|
|
533
535
|
updateStore = { _ ->
|
|
534
|
-
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_width", width)
|
|
535
|
-
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_height", height)
|
|
536
|
-
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", fps)
|
|
536
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_width", defaults.width)
|
|
537
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_height", defaults.height)
|
|
538
|
+
DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_video_fps", defaults.fps)
|
|
537
539
|
},
|
|
538
540
|
send = { requestId ->
|
|
539
|
-
deviceManager.sendButtonVideoRecordingSettings(requestId, width, height, fps)
|
|
541
|
+
deviceManager.sendButtonVideoRecordingSettings(requestId, defaults.width, defaults.height, defaults.fps)
|
|
540
542
|
},
|
|
541
543
|
)
|
|
542
544
|
|
|
543
|
-
fun
|
|
544
|
-
setButtonVideoRecordingSettings(width = settings.width, height = settings.height, fps = settings.fps)
|
|
545
|
-
|
|
546
|
-
fun setButtonCameraLed(enabled: Boolean): SettingsAckEvent =
|
|
547
|
-
performSettingsCommand(
|
|
548
|
-
setting = "button_camera_led",
|
|
549
|
-
updateStore = { _ -> DeviceStore.set(ObservableStore.BLUETOOTH_CATEGORY, "button_camera_led", enabled) },
|
|
550
|
-
send = { requestId -> deviceManager.sendButtonCameraLedSetting(requestId, enabled) },
|
|
551
|
-
)
|
|
552
|
-
|
|
553
|
-
fun setButtonMaxRecordingTime(minutes: Int): SettingsAckEvent =
|
|
545
|
+
fun setMaxVideoRecordingDuration(minutes: Int): SettingsAckEvent =
|
|
554
546
|
performSettingsCommand(
|
|
555
547
|
setting = "button_max_recording_time",
|
|
556
548
|
updateStore = { _ ->
|
|
@@ -574,6 +566,23 @@ class MentraBluetoothSdk private constructor(
|
|
|
574
566
|
return result
|
|
575
567
|
}
|
|
576
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Configure camera HAL tuning on Mentra Live glasses.
|
|
571
|
+
*
|
|
572
|
+
* The phone sends a {@code camera_tuning_config} BLE message; the ASG client relays it as a
|
|
573
|
+
* {@code camconfig} broadcast to SystemUI's CTReceiver so the camera HAL picks up the new
|
|
574
|
+
* parameters without a reboot.
|
|
575
|
+
*
|
|
576
|
+
* @param anrOn {@code true} = ANR enabled, {@code false} = ANR disabled (pixsmart param)
|
|
577
|
+
* @param gainOn {@code true} = stock gain params, {@code false} = pixsmart gain-off params
|
|
578
|
+
*/
|
|
579
|
+
fun setCameraTuningConfig(anrOn: Boolean, gainOn: Boolean): SettingsAckEvent =
|
|
580
|
+
performSettingsCommand(
|
|
581
|
+
setting = "camera_tuning",
|
|
582
|
+
updateStore = { _ -> },
|
|
583
|
+
send = { requestId -> deviceManager.sendCameraTuningConfig(requestId, anrOn, gainOn) },
|
|
584
|
+
)
|
|
585
|
+
|
|
577
586
|
fun setMicState(
|
|
578
587
|
enabled: Boolean,
|
|
579
588
|
useGlassesMic: Boolean = true,
|
|
@@ -623,18 +632,37 @@ class MentraBluetoothSdk private constructor(
|
|
|
623
632
|
|
|
624
633
|
fun requestWifiScan(): List<WifiScanResult> {
|
|
625
634
|
val pending = PendingResponse<List<WifiScanResult>>("WiFi scan request")
|
|
635
|
+
val request = PendingWifiScan(pending)
|
|
626
636
|
synchronized(oneShotLock) {
|
|
627
637
|
if (pendingWifiScan != null) {
|
|
628
|
-
throw
|
|
638
|
+
throw BluetoothSdkException(
|
|
629
639
|
"request_in_flight",
|
|
630
640
|
"A WiFi scan is already waiting for a glasses response.",
|
|
631
641
|
)
|
|
632
642
|
}
|
|
633
|
-
pendingWifiScan =
|
|
643
|
+
pendingWifiScan = request
|
|
634
644
|
}
|
|
635
645
|
try {
|
|
636
646
|
deviceManager.requestWifiScan()
|
|
637
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
|
+
}
|
|
661
|
+
if (fallbackResults.isNotEmpty()) {
|
|
662
|
+
return fallbackResults
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
throw error
|
|
638
666
|
} finally {
|
|
639
667
|
synchronized(oneShotLock) {
|
|
640
668
|
if (pendingWifiScan?.pending === pending) {
|
|
@@ -648,7 +676,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
648
676
|
val pending = PendingResponse<WifiStatusEvent>("WiFi connect request")
|
|
649
677
|
synchronized(oneShotLock) {
|
|
650
678
|
if (pendingWifiStatus != null) {
|
|
651
|
-
throw
|
|
679
|
+
throw BluetoothSdkException(
|
|
652
680
|
"request_in_flight",
|
|
653
681
|
"A WiFi status command is already waiting for a glasses response.",
|
|
654
682
|
)
|
|
@@ -671,7 +699,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
671
699
|
val pending = PendingResponse<WifiStatusEvent>("WiFi forget request")
|
|
672
700
|
synchronized(oneShotLock) {
|
|
673
701
|
if (pendingWifiStatus != null) {
|
|
674
|
-
throw
|
|
702
|
+
throw BluetoothSdkException(
|
|
675
703
|
"request_in_flight",
|
|
676
704
|
"A WiFi status command is already waiting for a glasses response.",
|
|
677
705
|
)
|
|
@@ -694,7 +722,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
694
722
|
val pending = PendingResponse<HotspotStatusEvent>("hotspot ${if (enabled) "enable" else "disable"} request")
|
|
695
723
|
synchronized(oneShotLock) {
|
|
696
724
|
if (pendingHotspotStatus != null) {
|
|
697
|
-
throw
|
|
725
|
+
throw BluetoothSdkException(
|
|
698
726
|
"request_in_flight",
|
|
699
727
|
"A hotspot command is already waiting for a glasses response.",
|
|
700
728
|
)
|
|
@@ -718,16 +746,44 @@ class MentraBluetoothSdk private constructor(
|
|
|
718
746
|
}
|
|
719
747
|
|
|
720
748
|
fun requestPhoto(request: PhotoRequest): PhotoResponseEvent {
|
|
749
|
+
val routedRequest =
|
|
750
|
+
nonBlankRequestId(request.requestId)
|
|
751
|
+
?.let { if (it == request.requestId) request else request.copy(requestId = it) }
|
|
752
|
+
?: request.copy(requestId = generatedCameraRequestId("photo"))
|
|
721
753
|
Bridge.log(
|
|
722
|
-
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=${
|
|
754
|
+
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=${routedRequest.requestId}"
|
|
723
755
|
)
|
|
724
|
-
val pending = PendingResponse<PhotoResponseEvent>("photo request ${
|
|
725
|
-
pendingPhotoRequests[
|
|
756
|
+
val pending = PendingResponse<PhotoResponseEvent>("photo request ${routedRequest.requestId}")
|
|
757
|
+
pendingPhotoRequests[routedRequest.requestId] = pending
|
|
758
|
+
try {
|
|
759
|
+
deviceManager.requestPhoto(routedRequest)
|
|
760
|
+
return pending.await()
|
|
761
|
+
} finally {
|
|
762
|
+
pendingPhotoRequests.remove(routedRequest.requestId, pending)
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
fun warmUpCamera(
|
|
767
|
+
requestId: String? = null,
|
|
768
|
+
size: PhotoSize,
|
|
769
|
+
exposureTimeNs: Long?,
|
|
770
|
+
durationMs: Int,
|
|
771
|
+
): CameraStatusEvent {
|
|
772
|
+
val effectiveRequestId = nonBlankRequestId(requestId) ?: generatedCameraRequestId("warm")
|
|
773
|
+
val pending = PendingResponse<CameraStatusEvent>("camera warm up $effectiveRequestId")
|
|
774
|
+
// Guard against a concurrent warm-up reusing the same requestId: a blind put would overwrite
|
|
775
|
+
// (and strand) the first caller's continuation until it times out. Reject the duplicate.
|
|
776
|
+
if (pendingCameraStatusRequests.putIfAbsent(effectiveRequestId, pending) != null) {
|
|
777
|
+
throw BluetoothSdkException(
|
|
778
|
+
"request_in_flight",
|
|
779
|
+
"A camera warm-up request with this requestId is already waiting for a glasses response.",
|
|
780
|
+
)
|
|
781
|
+
}
|
|
726
782
|
try {
|
|
727
|
-
deviceManager.
|
|
783
|
+
deviceManager.warmUpCamera(effectiveRequestId, size, exposureTimeNs, durationMs)
|
|
728
784
|
return pending.await()
|
|
729
785
|
} finally {
|
|
730
|
-
|
|
786
|
+
pendingCameraStatusRequests.remove(effectiveRequestId, pending)
|
|
731
787
|
}
|
|
732
788
|
}
|
|
733
789
|
|
|
@@ -735,7 +791,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
735
791
|
val pending = PendingResponse<GalleryStatusEvent>("gallery status query")
|
|
736
792
|
synchronized(oneShotLock) {
|
|
737
793
|
if (pendingGalleryStatus != null) {
|
|
738
|
-
throw
|
|
794
|
+
throw BluetoothSdkException(
|
|
739
795
|
"request_in_flight",
|
|
740
796
|
"A gallery status query is already waiting for a glasses response.",
|
|
741
797
|
)
|
|
@@ -754,7 +810,16 @@ class MentraBluetoothSdk private constructor(
|
|
|
754
810
|
}
|
|
755
811
|
}
|
|
756
812
|
|
|
757
|
-
fun startStream(request: StreamRequest): StreamStatusEvent
|
|
813
|
+
fun startStream(request: StreamRequest): StreamStatusEvent =
|
|
814
|
+
startStream(request, startSdkKeepAlive = true)
|
|
815
|
+
|
|
816
|
+
internal fun startExternallyManagedStream(request: StreamRequest): StreamStatusEvent =
|
|
817
|
+
startStream(request, startSdkKeepAlive = false)
|
|
818
|
+
|
|
819
|
+
private fun startStream(
|
|
820
|
+
request: StreamRequest,
|
|
821
|
+
startSdkKeepAlive: Boolean,
|
|
822
|
+
): StreamStatusEvent {
|
|
758
823
|
val message = request.toMap().toMutableMap()
|
|
759
824
|
val streamId = (message["streamId"] as? String)?.takeIf { it.isNotBlank() }
|
|
760
825
|
?: "sdk-${UUID.randomUUID()}"
|
|
@@ -765,8 +830,8 @@ class MentraBluetoothSdk private constructor(
|
|
|
765
830
|
try {
|
|
766
831
|
deviceManager.startStream(message)
|
|
767
832
|
val event = pending.await(STREAM_START_TIMEOUT_MS)
|
|
768
|
-
if (
|
|
769
|
-
startStreamKeepAliveMonitor(streamId,
|
|
833
|
+
if (startSdkKeepAlive) {
|
|
834
|
+
startStreamKeepAliveMonitor(streamId, DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS)
|
|
770
835
|
}
|
|
771
836
|
return event
|
|
772
837
|
} finally {
|
|
@@ -804,7 +869,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
804
869
|
}
|
|
805
870
|
synchronized(oneShotLock) {
|
|
806
871
|
if (pendingStreamStop != null) {
|
|
807
|
-
throw
|
|
872
|
+
throw BluetoothSdkException(
|
|
808
873
|
"request_in_flight",
|
|
809
874
|
"A stream stop command is already waiting for a glasses response.",
|
|
810
875
|
)
|
|
@@ -830,7 +895,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
830
895
|
val pending = PendingResponse<VideoRecordingStatusEvent>("start video recording")
|
|
831
896
|
val pendingRequest = PendingVideoRecordingRequest("recording_started", pending)
|
|
832
897
|
if (pendingVideoRecordingRequests.putIfAbsent(request.requestId, pendingRequest) != null) {
|
|
833
|
-
throw
|
|
898
|
+
throw BluetoothSdkException(
|
|
834
899
|
"request_in_flight",
|
|
835
900
|
"A video recording command is already waiting for requestId ${request.requestId}.",
|
|
836
901
|
)
|
|
@@ -868,7 +933,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
868
933
|
waitForUpload = waitForUpload,
|
|
869
934
|
)
|
|
870
935
|
if (pendingVideoRecordingRequests.putIfAbsent(requestId, pendingRequest) != null) {
|
|
871
|
-
throw
|
|
936
|
+
throw BluetoothSdkException(
|
|
872
937
|
"request_in_flight",
|
|
873
938
|
"A video recording command is already waiting for requestId $requestId.",
|
|
874
939
|
)
|
|
@@ -887,7 +952,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
887
952
|
val pending = PendingResponse<VersionInfoResult>("version info request")
|
|
888
953
|
synchronized(oneShotLock) {
|
|
889
954
|
if (pendingVersionInfo != null) {
|
|
890
|
-
throw
|
|
955
|
+
throw BluetoothSdkException(
|
|
891
956
|
"request_in_flight",
|
|
892
957
|
"A version info request is already waiting for a glasses response.",
|
|
893
958
|
)
|
|
@@ -916,13 +981,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
916
981
|
fun checkForOtaUpdate(): Boolean {
|
|
917
982
|
val status = getFreshGlassesStatus()
|
|
918
983
|
if (!status.connected) {
|
|
919
|
-
throw
|
|
984
|
+
throw BluetoothSdkException(
|
|
920
985
|
"glasses_not_connected",
|
|
921
986
|
"Cannot check OTA update because glasses are not connected.",
|
|
922
987
|
)
|
|
923
988
|
}
|
|
924
989
|
if (status.buildNumber.isBlank()) {
|
|
925
|
-
throw
|
|
990
|
+
throw BluetoothSdkException(
|
|
926
991
|
"missing_glasses_version",
|
|
927
992
|
"Cannot check OTA update because glasses build number is unavailable.",
|
|
928
993
|
)
|
|
@@ -952,7 +1017,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
952
1017
|
val pending = PendingResponse<OtaQueryResult>(operation)
|
|
953
1018
|
synchronized(oneShotLock) {
|
|
954
1019
|
if (pendingOtaQuery != null) {
|
|
955
|
-
throw
|
|
1020
|
+
throw BluetoothSdkException(
|
|
956
1021
|
"request_in_flight",
|
|
957
1022
|
"An OTA status query is already waiting for a glasses response.",
|
|
958
1023
|
)
|
|
@@ -981,7 +1046,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
981
1046
|
val pending = PendingResponse<OtaStartAckEvent>("OTA start command")
|
|
982
1047
|
synchronized(oneShotLock) {
|
|
983
1048
|
if (pendingOtaStart != null) {
|
|
984
|
-
throw
|
|
1049
|
+
throw BluetoothSdkException(
|
|
985
1050
|
"request_in_flight",
|
|
986
1051
|
"An OTA start command is already waiting for a glasses response.",
|
|
987
1052
|
)
|
|
@@ -1005,13 +1070,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
1005
1070
|
|
|
1006
1071
|
internal fun sendOtaQueryStatus(): OtaQueryResult = queryOtaStatus()
|
|
1007
1072
|
|
|
1008
|
-
/** Re-run the glasses-side OTA version check after an internal clock-skew recovery. */
|
|
1009
|
-
@JvmSynthetic
|
|
1010
|
-
internal fun retryOtaVersionCheck(): OtaQueryResult =
|
|
1011
|
-
performOtaQuery("OTA version retry") {
|
|
1012
|
-
deviceManager.retryOtaVersionCheck()
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
1073
|
private fun getFreshGlassesStatus(): GlassesStatus {
|
|
1016
1074
|
val status = getRawGlassesStatus()
|
|
1017
1075
|
if (!status.connected || status.buildNumber.isNotBlank()) {
|
|
@@ -1053,7 +1111,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1053
1111
|
}
|
|
1054
1112
|
|
|
1055
1113
|
if (!status.connected) {
|
|
1056
|
-
throw
|
|
1114
|
+
throw BluetoothSdkException(
|
|
1057
1115
|
"glasses_not_connected",
|
|
1058
1116
|
"Cannot check OTA update because glasses disconnected.",
|
|
1059
1117
|
)
|
|
@@ -1081,7 +1139,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1081
1139
|
private fun resolveOtaVersionUrl(status: GlassesStatus): String {
|
|
1082
1140
|
val deviceUrl = status.otaVersionUrl.trim()
|
|
1083
1141
|
if (isLegacyAsgOtaStartBuild(status.buildNumber)) {
|
|
1084
|
-
return
|
|
1142
|
+
return OtaManifestDefaults.LEGACY_PROD_OTA_VERSION_URL
|
|
1085
1143
|
}
|
|
1086
1144
|
// SDK consumers are pinned to the manifest built for their SDK version.
|
|
1087
1145
|
// A future glasses-advertised URL should not silently change that pairing.
|
|
@@ -1090,7 +1148,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1090
1148
|
|
|
1091
1149
|
private fun isLegacyAsgOtaStartBuild(buildNumber: String): Boolean {
|
|
1092
1150
|
val parsed = buildNumber.toIntOrNull()
|
|
1093
|
-
return parsed != null && parsed <
|
|
1151
|
+
return parsed != null && parsed < 39
|
|
1094
1152
|
}
|
|
1095
1153
|
|
|
1096
1154
|
internal fun sendShutdown() {
|
|
@@ -1163,7 +1221,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1163
1221
|
val bluetoothManager = appContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
|
1164
1222
|
val adapter =
|
|
1165
1223
|
bluetoothManager?.adapter
|
|
1166
|
-
?: throw
|
|
1224
|
+
?: throw BluetoothSdkException(
|
|
1167
1225
|
"bluetooth_unsupported",
|
|
1168
1226
|
"This phone does not support Bluetooth.",
|
|
1169
1227
|
)
|
|
@@ -1171,14 +1229,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
1171
1229
|
try {
|
|
1172
1230
|
adapter.isEnabled
|
|
1173
1231
|
} catch (error: SecurityException) {
|
|
1174
|
-
throw
|
|
1232
|
+
throw BluetoothSdkException(
|
|
1175
1233
|
"bluetooth_permission_denied",
|
|
1176
1234
|
"Allow Bluetooth permission to $operation.",
|
|
1177
1235
|
error,
|
|
1178
1236
|
)
|
|
1179
1237
|
}
|
|
1180
1238
|
if (!enabled) {
|
|
1181
|
-
throw
|
|
1239
|
+
throw BluetoothSdkException(
|
|
1182
1240
|
"bluetooth_powered_off",
|
|
1183
1241
|
"Turn on phone Bluetooth to $operation.",
|
|
1184
1242
|
)
|
|
@@ -1186,7 +1244,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1186
1244
|
}
|
|
1187
1245
|
|
|
1188
1246
|
private fun Throwable.toBluetoothError(defaultCode: String): BluetoothError =
|
|
1189
|
-
if (this is
|
|
1247
|
+
if (this is BluetoothSdkException) {
|
|
1190
1248
|
BluetoothError(code, message ?: code, this)
|
|
1191
1249
|
} else {
|
|
1192
1250
|
BluetoothError(defaultCode, message ?: toString(), this)
|
|
@@ -1273,6 +1331,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1273
1331
|
data.containsKey("scanComplete") || data.containsKey("scan_complete")
|
|
1274
1332
|
val scanComplete =
|
|
1275
1333
|
(data["scanComplete"] as? Boolean) ?: (data["scan_complete"] as? Boolean) ?: false
|
|
1334
|
+
updateWifiScanLatestResults(networks)
|
|
1276
1335
|
if (scanComplete || !hasCompletionFlag) {
|
|
1277
1336
|
handleWifiScanResultsForRequests(networks)
|
|
1278
1337
|
}
|
|
@@ -1301,6 +1360,11 @@ class MentraBluetoothSdk private constructor(
|
|
|
1301
1360
|
dispatchToListeners { it.onPhotoResponse(event) }
|
|
1302
1361
|
}
|
|
1303
1362
|
"photo_status" -> dispatchToListeners { it.onPhotoStatus(PhotoStatusEvent(data)) }
|
|
1363
|
+
"camera_status" -> {
|
|
1364
|
+
val event = CameraStatusEvent(data)
|
|
1365
|
+
handleCameraStatusForRequests(event)
|
|
1366
|
+
dispatchToListeners { it.onCameraStatus(event) }
|
|
1367
|
+
}
|
|
1304
1368
|
"video_recording_status" -> {
|
|
1305
1369
|
val event = VideoRecordingStatusEvent(data)
|
|
1306
1370
|
handleVideoRecordingStatusForRequests(event)
|
|
@@ -1328,13 +1392,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
1328
1392
|
dispatchToListeners { it.onKeepAliveAck(event) }
|
|
1329
1393
|
}
|
|
1330
1394
|
}
|
|
1331
|
-
"ota_update_available" -> {
|
|
1332
|
-
val resultValues = data + mapOf("type" to "ota_update_available")
|
|
1333
|
-
synchronized(oneShotLock) {
|
|
1334
|
-
pendingOtaQuery?.resolve(OtaQueryResult(resultValues))
|
|
1335
|
-
}
|
|
1336
|
-
dispatchToListeners { it.onOtaUpdateAvailable(OtaUpdateAvailableEvent.fromMap(resultValues)) }
|
|
1337
|
-
}
|
|
1338
1395
|
"ota_start_ack" -> {
|
|
1339
1396
|
val event = OtaStartAckEvent.fromMap(data + mapOf("type" to "ota_start_ack"))
|
|
1340
1397
|
synchronized(oneShotLock) {
|
|
@@ -1524,10 +1581,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
1524
1581
|
)
|
|
1525
1582
|
)
|
|
1526
1583
|
|
|
1527
|
-
private fun streamStatusException(event: StreamStatusEvent, code: String):
|
|
1584
|
+
private fun streamStatusException(event: StreamStatusEvent, code: String): BluetoothSdkException {
|
|
1528
1585
|
val details = (event.status as? StreamStatus.Error)?.errorDetails
|
|
1529
1586
|
?: "Stream status ${event.state.value}"
|
|
1530
|
-
return
|
|
1587
|
+
return BluetoothSdkException(code, details)
|
|
1531
1588
|
}
|
|
1532
1589
|
|
|
1533
1590
|
private fun handlePhotoResponseForRequests(event: PhotoResponseEvent) {
|
|
@@ -1536,7 +1593,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1536
1593
|
is PhotoResponse.Success -> pending.resolve(event)
|
|
1537
1594
|
is PhotoResponse.Error ->
|
|
1538
1595
|
pending.reject(
|
|
1539
|
-
|
|
1596
|
+
BluetoothSdkException(
|
|
1540
1597
|
response.errorCode ?: "photo_request_failed",
|
|
1541
1598
|
response.errorMessage,
|
|
1542
1599
|
)
|
|
@@ -1544,6 +1601,22 @@ class MentraBluetoothSdk private constructor(
|
|
|
1544
1601
|
}
|
|
1545
1602
|
}
|
|
1546
1603
|
|
|
1604
|
+
private fun handleCameraStatusForRequests(event: CameraStatusEvent) {
|
|
1605
|
+
val pending = pendingCameraStatusRequests[event.requestId] ?: return
|
|
1606
|
+
when (event.state.lowercase()) {
|
|
1607
|
+
"ready" -> pending.resolve(event)
|
|
1608
|
+
"error" ->
|
|
1609
|
+
pending.reject(
|
|
1610
|
+
BluetoothSdkException(
|
|
1611
|
+
event.errorCode ?: "camera_warm_up_failed",
|
|
1612
|
+
event.errorMessage ?: "Camera warm-up failed.",
|
|
1613
|
+
)
|
|
1614
|
+
)
|
|
1615
|
+
// "warming"/"stopped" are progress updates; leave the pending promise alone.
|
|
1616
|
+
else -> {}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1547
1620
|
private fun handleVideoRecordingStatusForRequests(event: VideoRecordingStatusEvent) {
|
|
1548
1621
|
val request = pendingVideoRecordingRequests[event.requestId] ?: return
|
|
1549
1622
|
if (event.success) {
|
|
@@ -1559,7 +1632,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1559
1632
|
}
|
|
1560
1633
|
} else {
|
|
1561
1634
|
request.pending.reject(
|
|
1562
|
-
|
|
1635
|
+
BluetoothSdkException(
|
|
1563
1636
|
event.status.ifBlank { "video_recording_failed" },
|
|
1564
1637
|
event.details ?: "Video recording command failed.",
|
|
1565
1638
|
)
|
|
@@ -1580,7 +1653,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1580
1653
|
}
|
|
1581
1654
|
} else {
|
|
1582
1655
|
request.pending.reject(
|
|
1583
|
-
|
|
1656
|
+
BluetoothSdkException(
|
|
1584
1657
|
"video_upload_failed",
|
|
1585
1658
|
event.errorMessage ?: "Video upload failed.",
|
|
1586
1659
|
)
|
|
@@ -1594,7 +1667,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1594
1667
|
pending.resolve(event)
|
|
1595
1668
|
} else {
|
|
1596
1669
|
pending.reject(
|
|
1597
|
-
|
|
1670
|
+
BluetoothSdkException(
|
|
1598
1671
|
event.errorCode ?: "rgb_led_control_failed",
|
|
1599
1672
|
event.errorCode ?: "RGB LED command failed.",
|
|
1600
1673
|
)
|
|
@@ -1606,7 +1679,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1606
1679
|
val pending = pendingSettingsRequests[event.requestId] ?: return
|
|
1607
1680
|
if (isFailureStatus(event.status)) {
|
|
1608
1681
|
pending.reject(
|
|
1609
|
-
|
|
1682
|
+
BluetoothSdkException(
|
|
1610
1683
|
event.errorCode ?: "${event.setting.ifBlank { "settings" }}_failed",
|
|
1611
1684
|
event.errorMessage ?: "Settings command ${event.setting.ifBlank { event.requestId }} failed.",
|
|
1612
1685
|
)
|
|
@@ -1629,6 +1702,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
1629
1702
|
request.pending.resolve(results)
|
|
1630
1703
|
}
|
|
1631
1704
|
|
|
1705
|
+
private fun updateWifiScanLatestResults(results: List<WifiScanResult>) {
|
|
1706
|
+
if (results.isEmpty()) return
|
|
1707
|
+
synchronized(oneShotLock) {
|
|
1708
|
+
pendingWifiScan?.latestResults = results
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1632
1712
|
private fun handleWifiStatusForRequests(event: WifiStatusEvent) {
|
|
1633
1713
|
val request = synchronized(oneShotLock) { pendingWifiStatus } ?: return
|
|
1634
1714
|
if (!wifiStatusMatches(event.status, request)) return
|
|
@@ -1674,7 +1754,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1674
1754
|
}
|
|
1675
1755
|
}
|
|
1676
1756
|
request.pending.reject(
|
|
1677
|
-
|
|
1757
|
+
BluetoothSdkException(
|
|
1678
1758
|
"hotspot_command_failed",
|
|
1679
1759
|
event.message ?: "Hotspot command failed.",
|
|
1680
1760
|
)
|
|
@@ -9,14 +9,14 @@ import org.json.JSONObject
|
|
|
9
9
|
internal object OtaManifestDefaults {
|
|
10
10
|
private const val SDK_OTA_RELEASE_BASE_URL =
|
|
11
11
|
"https://github.com/Mentra-Community/MentraOS/releases/download/bluetooth-sdk-ota"
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
const val
|
|
12
|
+
// ASG builds before 39 ignore ota_start.ota_version_url, so SDK checks must
|
|
13
|
+
// use the same legacy production manifest those glasses will install from.
|
|
14
|
+
const val LEGACY_PROD_OTA_VERSION_URL = "https://ota.mentraglass.com/prod_live_version.json"
|
|
15
15
|
|
|
16
16
|
fun defaultOtaVersionUrl(): String {
|
|
17
17
|
val sdkVersion = BuildConfig.SDK_VERSION.trim()
|
|
18
18
|
if (sdkVersion.isBlank() || sdkVersion == "unspecified") {
|
|
19
|
-
throw
|
|
19
|
+
throw BluetoothSdkException(
|
|
20
20
|
"missing_sdk_version",
|
|
21
21
|
"Cannot determine Bluetooth SDK version for the default OTA manifest URL.",
|
|
22
22
|
)
|
|
@@ -31,19 +31,19 @@ internal object OtaManifestChecker {
|
|
|
31
31
|
fun normalizeHttpUrl(value: String): String {
|
|
32
32
|
val trimmed = value.trim()
|
|
33
33
|
if (trimmed.isEmpty()) {
|
|
34
|
-
throw
|
|
34
|
+
throw BluetoothSdkException("invalid_ota_url", "OTA version URL must be a non-empty http(s) URL.")
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
val uri =
|
|
38
38
|
try {
|
|
39
39
|
URI(trimmed)
|
|
40
40
|
} catch (error: Exception) {
|
|
41
|
-
throw
|
|
41
|
+
throw BluetoothSdkException("invalid_ota_url", "OTA version URL must be a valid http(s) URL.", error)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
val scheme = uri.scheme?.lowercase()
|
|
45
45
|
if ((scheme != "http" && scheme != "https") || uri.host.isNullOrBlank()) {
|
|
46
|
-
throw
|
|
46
|
+
throw BluetoothSdkException("invalid_ota_url", "OTA version URL must be an http(s) URL.")
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
return uri.toString()
|
|
@@ -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
|
|
60
|
+
throw BluetoothSdkException("ota_manifest_request_failed", "OTA manifest request failed with HTTP $status.")
|
|
61
61
|
}
|
|
62
62
|
JSONObject(connection.inputStream.bufferedReader().use { it.readText() })
|
|
63
63
|
} finally {
|
|
@@ -92,13 +92,13 @@ internal object OtaManifestChecker {
|
|
|
92
92
|
return manifest
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
throw
|
|
95
|
+
throw BluetoothSdkException("invalid_ota_manifest", "OTA manifest is missing ASG app versionCode.")
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
private fun hasApkUpdate(currentBuildNumber: String, manifest: JSONObject): Boolean {
|
|
99
99
|
val currentVersion =
|
|
100
100
|
currentBuildNumber.toLongOrNull()
|
|
101
|
-
?: throw
|
|
101
|
+
?: throw BluetoothSdkException(
|
|
102
102
|
"invalid_glasses_version",
|
|
103
103
|
"Cannot check OTA update because glasses build number is invalid.",
|
|
104
104
|
)
|
|
@@ -124,7 +124,7 @@ internal object OtaManifestChecker {
|
|
|
124
124
|
if (besFirmware == null) return false
|
|
125
125
|
val serverVersion = besFirmware.optString("version", "")
|
|
126
126
|
if (serverVersion.isBlank()) {
|
|
127
|
-
throw
|
|
127
|
+
throw BluetoothSdkException("invalid_ota_manifest", "OTA manifest bes_firmware.version is missing.")
|
|
128
128
|
}
|
|
129
129
|
if (currentVersion.isBlank()) return true
|
|
130
130
|
return compareVersions(serverVersion, currentVersion) > 0
|
|
@@ -149,5 +149,5 @@ internal object OtaManifestChecker {
|
|
|
149
149
|
|
|
150
150
|
private fun JSONObject.requiredLong(key: String): Long =
|
|
151
151
|
(opt(key) as? Number)?.toLong()
|
|
152
|
-
?: throw
|
|
152
|
+
?: throw BluetoothSdkException("invalid_ota_manifest", "OTA manifest is missing ASG app versionCode.")
|
|
153
153
|
}
|