@mentra/bluetooth-sdk 0.1.14 → 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 +4 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +22 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +5 -20
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +358 -261
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +3 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +133 -52
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +12 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +22 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -22
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +50 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1184 -795
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +422 -78
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +3 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +2532 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -1
- 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 +39 -14
- 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 +11 -3
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +5 -0
- 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 +8 -2
- 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 +72 -54
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +40 -12
- package/ios/Source/Bridge.swift +4 -16
- package/ios/Source/DeviceManager.swift +72 -12
- package/ios/Source/MentraBluetoothSDK.swift +120 -50
- package/ios/Source/OtaManifest.swift +13 -13
- package/ios/Source/camera/CameraModels.swift +75 -9
- 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/G1.swift +51 -1
- package/ios/Source/sgcs/G2.swift +643 -203
- package/ios/Source/sgcs/MentraLive.swift +53 -39
- package/ios/Source/sgcs/Nimo.swift +1928 -0
- package/ios/Source/sgcs/SGCManager.swift +12 -5
- 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 +42 -17
- package/src/_internal.ts +4 -4
- package/src/_private/BluetoothSdkModule.ts +18 -2
- package/src/_private/cameraRequestPayload.ts +29 -0
- package/src/_private/photoRequestPayload.ts +9 -2
- package/src/index.ts +76 -58
|
@@ -142,7 +142,7 @@ object DeviceStore {
|
|
|
142
142
|
store.set("bluetooth", "use_native_dashboard", false)
|
|
143
143
|
// Mentra Nex feature flags (off by default; toggled from Nex Developer Settings):
|
|
144
144
|
store.set("bluetooth", "nex_chinese_captions", false)
|
|
145
|
-
store.set("bluetooth", "
|
|
145
|
+
store.set("bluetooth", "nex_lc3_audio_playback", false)
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
fun get(category: String, key: String): Any? {
|
|
@@ -268,9 +268,9 @@ object DeviceStore {
|
|
|
268
268
|
"bluetooth" to "voice_activity_detection_enabled" -> {
|
|
269
269
|
DeviceManager.getInstance().sgc?.sendVoiceActivityDetectionSetting()
|
|
270
270
|
}
|
|
271
|
-
"bluetooth" to "
|
|
271
|
+
"bluetooth" to "nex_lc3_audio_playback" -> {
|
|
272
272
|
(value as? Boolean)?.let { enabled ->
|
|
273
|
-
Bridge.log("DeviceStore:
|
|
273
|
+
Bridge.log("DeviceStore: nex_lc3_audio_playback changed to $enabled")
|
|
274
274
|
DeviceManager.getInstance().sgc?.applyNexAudioPlaybackSetting()
|
|
275
275
|
}
|
|
276
276
|
}
|
|
@@ -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
|
)
|
|
@@ -564,6 +566,23 @@ class MentraBluetoothSdk private constructor(
|
|
|
564
566
|
return result
|
|
565
567
|
}
|
|
566
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
|
+
|
|
567
586
|
fun setMicState(
|
|
568
587
|
enabled: Boolean,
|
|
569
588
|
useGlassesMic: Boolean = true,
|
|
@@ -613,18 +632,37 @@ class MentraBluetoothSdk private constructor(
|
|
|
613
632
|
|
|
614
633
|
fun requestWifiScan(): List<WifiScanResult> {
|
|
615
634
|
val pending = PendingResponse<List<WifiScanResult>>("WiFi scan request")
|
|
635
|
+
val request = PendingWifiScan(pending)
|
|
616
636
|
synchronized(oneShotLock) {
|
|
617
637
|
if (pendingWifiScan != null) {
|
|
618
|
-
throw
|
|
638
|
+
throw BluetoothSdkException(
|
|
619
639
|
"request_in_flight",
|
|
620
640
|
"A WiFi scan is already waiting for a glasses response.",
|
|
621
641
|
)
|
|
622
642
|
}
|
|
623
|
-
pendingWifiScan =
|
|
643
|
+
pendingWifiScan = request
|
|
624
644
|
}
|
|
625
645
|
try {
|
|
626
646
|
deviceManager.requestWifiScan()
|
|
627
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
|
|
628
666
|
} finally {
|
|
629
667
|
synchronized(oneShotLock) {
|
|
630
668
|
if (pendingWifiScan?.pending === pending) {
|
|
@@ -638,7 +676,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
638
676
|
val pending = PendingResponse<WifiStatusEvent>("WiFi connect request")
|
|
639
677
|
synchronized(oneShotLock) {
|
|
640
678
|
if (pendingWifiStatus != null) {
|
|
641
|
-
throw
|
|
679
|
+
throw BluetoothSdkException(
|
|
642
680
|
"request_in_flight",
|
|
643
681
|
"A WiFi status command is already waiting for a glasses response.",
|
|
644
682
|
)
|
|
@@ -661,7 +699,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
661
699
|
val pending = PendingResponse<WifiStatusEvent>("WiFi forget request")
|
|
662
700
|
synchronized(oneShotLock) {
|
|
663
701
|
if (pendingWifiStatus != null) {
|
|
664
|
-
throw
|
|
702
|
+
throw BluetoothSdkException(
|
|
665
703
|
"request_in_flight",
|
|
666
704
|
"A WiFi status command is already waiting for a glasses response.",
|
|
667
705
|
)
|
|
@@ -684,7 +722,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
684
722
|
val pending = PendingResponse<HotspotStatusEvent>("hotspot ${if (enabled) "enable" else "disable"} request")
|
|
685
723
|
synchronized(oneShotLock) {
|
|
686
724
|
if (pendingHotspotStatus != null) {
|
|
687
|
-
throw
|
|
725
|
+
throw BluetoothSdkException(
|
|
688
726
|
"request_in_flight",
|
|
689
727
|
"A hotspot command is already waiting for a glasses response.",
|
|
690
728
|
)
|
|
@@ -708,16 +746,44 @@ class MentraBluetoothSdk private constructor(
|
|
|
708
746
|
}
|
|
709
747
|
|
|
710
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"))
|
|
711
753
|
Bridge.log(
|
|
712
|
-
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=${
|
|
754
|
+
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=${routedRequest.requestId}"
|
|
713
755
|
)
|
|
714
|
-
val pending = PendingResponse<PhotoResponseEvent>("photo request ${
|
|
715
|
-
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
|
+
}
|
|
716
782
|
try {
|
|
717
|
-
deviceManager.
|
|
783
|
+
deviceManager.warmUpCamera(effectiveRequestId, size, exposureTimeNs, durationMs)
|
|
718
784
|
return pending.await()
|
|
719
785
|
} finally {
|
|
720
|
-
|
|
786
|
+
pendingCameraStatusRequests.remove(effectiveRequestId, pending)
|
|
721
787
|
}
|
|
722
788
|
}
|
|
723
789
|
|
|
@@ -725,7 +791,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
725
791
|
val pending = PendingResponse<GalleryStatusEvent>("gallery status query")
|
|
726
792
|
synchronized(oneShotLock) {
|
|
727
793
|
if (pendingGalleryStatus != null) {
|
|
728
|
-
throw
|
|
794
|
+
throw BluetoothSdkException(
|
|
729
795
|
"request_in_flight",
|
|
730
796
|
"A gallery status query is already waiting for a glasses response.",
|
|
731
797
|
)
|
|
@@ -803,7 +869,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
803
869
|
}
|
|
804
870
|
synchronized(oneShotLock) {
|
|
805
871
|
if (pendingStreamStop != null) {
|
|
806
|
-
throw
|
|
872
|
+
throw BluetoothSdkException(
|
|
807
873
|
"request_in_flight",
|
|
808
874
|
"A stream stop command is already waiting for a glasses response.",
|
|
809
875
|
)
|
|
@@ -829,7 +895,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
829
895
|
val pending = PendingResponse<VideoRecordingStatusEvent>("start video recording")
|
|
830
896
|
val pendingRequest = PendingVideoRecordingRequest("recording_started", pending)
|
|
831
897
|
if (pendingVideoRecordingRequests.putIfAbsent(request.requestId, pendingRequest) != null) {
|
|
832
|
-
throw
|
|
898
|
+
throw BluetoothSdkException(
|
|
833
899
|
"request_in_flight",
|
|
834
900
|
"A video recording command is already waiting for requestId ${request.requestId}.",
|
|
835
901
|
)
|
|
@@ -867,7 +933,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
867
933
|
waitForUpload = waitForUpload,
|
|
868
934
|
)
|
|
869
935
|
if (pendingVideoRecordingRequests.putIfAbsent(requestId, pendingRequest) != null) {
|
|
870
|
-
throw
|
|
936
|
+
throw BluetoothSdkException(
|
|
871
937
|
"request_in_flight",
|
|
872
938
|
"A video recording command is already waiting for requestId $requestId.",
|
|
873
939
|
)
|
|
@@ -886,7 +952,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
886
952
|
val pending = PendingResponse<VersionInfoResult>("version info request")
|
|
887
953
|
synchronized(oneShotLock) {
|
|
888
954
|
if (pendingVersionInfo != null) {
|
|
889
|
-
throw
|
|
955
|
+
throw BluetoothSdkException(
|
|
890
956
|
"request_in_flight",
|
|
891
957
|
"A version info request is already waiting for a glasses response.",
|
|
892
958
|
)
|
|
@@ -915,13 +981,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
915
981
|
fun checkForOtaUpdate(): Boolean {
|
|
916
982
|
val status = getFreshGlassesStatus()
|
|
917
983
|
if (!status.connected) {
|
|
918
|
-
throw
|
|
984
|
+
throw BluetoothSdkException(
|
|
919
985
|
"glasses_not_connected",
|
|
920
986
|
"Cannot check OTA update because glasses are not connected.",
|
|
921
987
|
)
|
|
922
988
|
}
|
|
923
989
|
if (status.buildNumber.isBlank()) {
|
|
924
|
-
throw
|
|
990
|
+
throw BluetoothSdkException(
|
|
925
991
|
"missing_glasses_version",
|
|
926
992
|
"Cannot check OTA update because glasses build number is unavailable.",
|
|
927
993
|
)
|
|
@@ -951,7 +1017,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
951
1017
|
val pending = PendingResponse<OtaQueryResult>(operation)
|
|
952
1018
|
synchronized(oneShotLock) {
|
|
953
1019
|
if (pendingOtaQuery != null) {
|
|
954
|
-
throw
|
|
1020
|
+
throw BluetoothSdkException(
|
|
955
1021
|
"request_in_flight",
|
|
956
1022
|
"An OTA status query is already waiting for a glasses response.",
|
|
957
1023
|
)
|
|
@@ -980,7 +1046,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
980
1046
|
val pending = PendingResponse<OtaStartAckEvent>("OTA start command")
|
|
981
1047
|
synchronized(oneShotLock) {
|
|
982
1048
|
if (pendingOtaStart != null) {
|
|
983
|
-
throw
|
|
1049
|
+
throw BluetoothSdkException(
|
|
984
1050
|
"request_in_flight",
|
|
985
1051
|
"An OTA start command is already waiting for a glasses response.",
|
|
986
1052
|
)
|
|
@@ -1004,13 +1070,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
1004
1070
|
|
|
1005
1071
|
internal fun sendOtaQueryStatus(): OtaQueryResult = queryOtaStatus()
|
|
1006
1072
|
|
|
1007
|
-
/** Re-run the glasses-side OTA version check after an internal clock-skew recovery. */
|
|
1008
|
-
@JvmSynthetic
|
|
1009
|
-
internal fun retryOtaVersionCheck(): OtaQueryResult =
|
|
1010
|
-
performOtaQuery("OTA version retry") {
|
|
1011
|
-
deviceManager.retryOtaVersionCheck()
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
1073
|
private fun getFreshGlassesStatus(): GlassesStatus {
|
|
1015
1074
|
val status = getRawGlassesStatus()
|
|
1016
1075
|
if (!status.connected || status.buildNumber.isNotBlank()) {
|
|
@@ -1052,7 +1111,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1052
1111
|
}
|
|
1053
1112
|
|
|
1054
1113
|
if (!status.connected) {
|
|
1055
|
-
throw
|
|
1114
|
+
throw BluetoothSdkException(
|
|
1056
1115
|
"glasses_not_connected",
|
|
1057
1116
|
"Cannot check OTA update because glasses disconnected.",
|
|
1058
1117
|
)
|
|
@@ -1080,7 +1139,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1080
1139
|
private fun resolveOtaVersionUrl(status: GlassesStatus): String {
|
|
1081
1140
|
val deviceUrl = status.otaVersionUrl.trim()
|
|
1082
1141
|
if (isLegacyAsgOtaStartBuild(status.buildNumber)) {
|
|
1083
|
-
return
|
|
1142
|
+
return OtaManifestDefaults.LEGACY_PROD_OTA_VERSION_URL
|
|
1084
1143
|
}
|
|
1085
1144
|
// SDK consumers are pinned to the manifest built for their SDK version.
|
|
1086
1145
|
// A future glasses-advertised URL should not silently change that pairing.
|
|
@@ -1089,7 +1148,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1089
1148
|
|
|
1090
1149
|
private fun isLegacyAsgOtaStartBuild(buildNumber: String): Boolean {
|
|
1091
1150
|
val parsed = buildNumber.toIntOrNull()
|
|
1092
|
-
return parsed != null && parsed <
|
|
1151
|
+
return parsed != null && parsed < 39
|
|
1093
1152
|
}
|
|
1094
1153
|
|
|
1095
1154
|
internal fun sendShutdown() {
|
|
@@ -1162,7 +1221,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1162
1221
|
val bluetoothManager = appContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
|
1163
1222
|
val adapter =
|
|
1164
1223
|
bluetoothManager?.adapter
|
|
1165
|
-
?: throw
|
|
1224
|
+
?: throw BluetoothSdkException(
|
|
1166
1225
|
"bluetooth_unsupported",
|
|
1167
1226
|
"This phone does not support Bluetooth.",
|
|
1168
1227
|
)
|
|
@@ -1170,14 +1229,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
1170
1229
|
try {
|
|
1171
1230
|
adapter.isEnabled
|
|
1172
1231
|
} catch (error: SecurityException) {
|
|
1173
|
-
throw
|
|
1232
|
+
throw BluetoothSdkException(
|
|
1174
1233
|
"bluetooth_permission_denied",
|
|
1175
1234
|
"Allow Bluetooth permission to $operation.",
|
|
1176
1235
|
error,
|
|
1177
1236
|
)
|
|
1178
1237
|
}
|
|
1179
1238
|
if (!enabled) {
|
|
1180
|
-
throw
|
|
1239
|
+
throw BluetoothSdkException(
|
|
1181
1240
|
"bluetooth_powered_off",
|
|
1182
1241
|
"Turn on phone Bluetooth to $operation.",
|
|
1183
1242
|
)
|
|
@@ -1185,7 +1244,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1185
1244
|
}
|
|
1186
1245
|
|
|
1187
1246
|
private fun Throwable.toBluetoothError(defaultCode: String): BluetoothError =
|
|
1188
|
-
if (this is
|
|
1247
|
+
if (this is BluetoothSdkException) {
|
|
1189
1248
|
BluetoothError(code, message ?: code, this)
|
|
1190
1249
|
} else {
|
|
1191
1250
|
BluetoothError(defaultCode, message ?: toString(), this)
|
|
@@ -1272,6 +1331,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1272
1331
|
data.containsKey("scanComplete") || data.containsKey("scan_complete")
|
|
1273
1332
|
val scanComplete =
|
|
1274
1333
|
(data["scanComplete"] as? Boolean) ?: (data["scan_complete"] as? Boolean) ?: false
|
|
1334
|
+
updateWifiScanLatestResults(networks)
|
|
1275
1335
|
if (scanComplete || !hasCompletionFlag) {
|
|
1276
1336
|
handleWifiScanResultsForRequests(networks)
|
|
1277
1337
|
}
|
|
@@ -1300,6 +1360,11 @@ class MentraBluetoothSdk private constructor(
|
|
|
1300
1360
|
dispatchToListeners { it.onPhotoResponse(event) }
|
|
1301
1361
|
}
|
|
1302
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
|
+
}
|
|
1303
1368
|
"video_recording_status" -> {
|
|
1304
1369
|
val event = VideoRecordingStatusEvent(data)
|
|
1305
1370
|
handleVideoRecordingStatusForRequests(event)
|
|
@@ -1327,13 +1392,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
1327
1392
|
dispatchToListeners { it.onKeepAliveAck(event) }
|
|
1328
1393
|
}
|
|
1329
1394
|
}
|
|
1330
|
-
"ota_update_available" -> {
|
|
1331
|
-
val resultValues = data + mapOf("type" to "ota_update_available")
|
|
1332
|
-
synchronized(oneShotLock) {
|
|
1333
|
-
pendingOtaQuery?.resolve(OtaQueryResult(resultValues))
|
|
1334
|
-
}
|
|
1335
|
-
dispatchToListeners { it.onOtaUpdateAvailable(OtaUpdateAvailableEvent.fromMap(resultValues)) }
|
|
1336
|
-
}
|
|
1337
1395
|
"ota_start_ack" -> {
|
|
1338
1396
|
val event = OtaStartAckEvent.fromMap(data + mapOf("type" to "ota_start_ack"))
|
|
1339
1397
|
synchronized(oneShotLock) {
|
|
@@ -1523,10 +1581,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
1523
1581
|
)
|
|
1524
1582
|
)
|
|
1525
1583
|
|
|
1526
|
-
private fun streamStatusException(event: StreamStatusEvent, code: String):
|
|
1584
|
+
private fun streamStatusException(event: StreamStatusEvent, code: String): BluetoothSdkException {
|
|
1527
1585
|
val details = (event.status as? StreamStatus.Error)?.errorDetails
|
|
1528
1586
|
?: "Stream status ${event.state.value}"
|
|
1529
|
-
return
|
|
1587
|
+
return BluetoothSdkException(code, details)
|
|
1530
1588
|
}
|
|
1531
1589
|
|
|
1532
1590
|
private fun handlePhotoResponseForRequests(event: PhotoResponseEvent) {
|
|
@@ -1535,7 +1593,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1535
1593
|
is PhotoResponse.Success -> pending.resolve(event)
|
|
1536
1594
|
is PhotoResponse.Error ->
|
|
1537
1595
|
pending.reject(
|
|
1538
|
-
|
|
1596
|
+
BluetoothSdkException(
|
|
1539
1597
|
response.errorCode ?: "photo_request_failed",
|
|
1540
1598
|
response.errorMessage,
|
|
1541
1599
|
)
|
|
@@ -1543,6 +1601,22 @@ class MentraBluetoothSdk private constructor(
|
|
|
1543
1601
|
}
|
|
1544
1602
|
}
|
|
1545
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
|
+
|
|
1546
1620
|
private fun handleVideoRecordingStatusForRequests(event: VideoRecordingStatusEvent) {
|
|
1547
1621
|
val request = pendingVideoRecordingRequests[event.requestId] ?: return
|
|
1548
1622
|
if (event.success) {
|
|
@@ -1558,7 +1632,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1558
1632
|
}
|
|
1559
1633
|
} else {
|
|
1560
1634
|
request.pending.reject(
|
|
1561
|
-
|
|
1635
|
+
BluetoothSdkException(
|
|
1562
1636
|
event.status.ifBlank { "video_recording_failed" },
|
|
1563
1637
|
event.details ?: "Video recording command failed.",
|
|
1564
1638
|
)
|
|
@@ -1579,7 +1653,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1579
1653
|
}
|
|
1580
1654
|
} else {
|
|
1581
1655
|
request.pending.reject(
|
|
1582
|
-
|
|
1656
|
+
BluetoothSdkException(
|
|
1583
1657
|
"video_upload_failed",
|
|
1584
1658
|
event.errorMessage ?: "Video upload failed.",
|
|
1585
1659
|
)
|
|
@@ -1593,7 +1667,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1593
1667
|
pending.resolve(event)
|
|
1594
1668
|
} else {
|
|
1595
1669
|
pending.reject(
|
|
1596
|
-
|
|
1670
|
+
BluetoothSdkException(
|
|
1597
1671
|
event.errorCode ?: "rgb_led_control_failed",
|
|
1598
1672
|
event.errorCode ?: "RGB LED command failed.",
|
|
1599
1673
|
)
|
|
@@ -1605,7 +1679,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1605
1679
|
val pending = pendingSettingsRequests[event.requestId] ?: return
|
|
1606
1680
|
if (isFailureStatus(event.status)) {
|
|
1607
1681
|
pending.reject(
|
|
1608
|
-
|
|
1682
|
+
BluetoothSdkException(
|
|
1609
1683
|
event.errorCode ?: "${event.setting.ifBlank { "settings" }}_failed",
|
|
1610
1684
|
event.errorMessage ?: "Settings command ${event.setting.ifBlank { event.requestId }} failed.",
|
|
1611
1685
|
)
|
|
@@ -1628,6 +1702,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
1628
1702
|
request.pending.resolve(results)
|
|
1629
1703
|
}
|
|
1630
1704
|
|
|
1705
|
+
private fun updateWifiScanLatestResults(results: List<WifiScanResult>) {
|
|
1706
|
+
if (results.isEmpty()) return
|
|
1707
|
+
synchronized(oneShotLock) {
|
|
1708
|
+
pendingWifiScan?.latestResults = results
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1631
1712
|
private fun handleWifiStatusForRequests(event: WifiStatusEvent) {
|
|
1632
1713
|
val request = synchronized(oneShotLock) { pendingWifiStatus } ?: return
|
|
1633
1714
|
if (!wifiStatusMatches(event.status, request)) return
|
|
@@ -1673,7 +1754,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1673
1754
|
}
|
|
1674
1755
|
}
|
|
1675
1756
|
request.pending.reject(
|
|
1676
|
-
|
|
1757
|
+
BluetoothSdkException(
|
|
1677
1758
|
"hotspot_command_failed",
|
|
1678
1759
|
event.message ?: "Hotspot command failed.",
|
|
1679
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
|
}
|
|
@@ -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"),
|
|
@@ -138,13 +145,13 @@ data class CameraFovResult(
|
|
|
138
145
|
fallback: CameraFov,
|
|
139
146
|
): CameraFovResult {
|
|
140
147
|
if (ack.status == "error") {
|
|
141
|
-
throw
|
|
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
|
|
154
|
+
throw BluetoothSdkException(
|
|
148
155
|
"camera_fov_not_applied",
|
|
149
156
|
"Camera FOV was saved but not applied to hardware.",
|
|
150
157
|
)
|
|
@@ -161,8 +168,7 @@ 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,
|
|
@@ -211,8 +217,8 @@ data class PhotoRequest @JvmOverloads constructor(
|
|
|
211
217
|
val ispAnalogGain = stringValue(values, "ispAnalogGain")
|
|
212
218
|
|
|
213
219
|
return PhotoRequest(
|
|
214
|
-
requestId = stringValue(values, "requestId", "request_id")
|
|
215
|
-
|
|
220
|
+
requestId = nonBlankRequestId(stringValue(values, "requestId", "request_id"))
|
|
221
|
+
?: generatedCameraRequestId("photo"),
|
|
216
222
|
size = PhotoSize.fromValue(stringValue(values, "size") ?: "medium"),
|
|
217
223
|
webhookUrl = stringValue(values, "webhookUrl", "webhook_url").orEmpty(),
|
|
218
224
|
authToken = stringValue(values, "authToken", "auth_token")?.takeIf { it.isNotBlank() },
|
|
@@ -439,3 +445,13 @@ data class PhotoStatusEvent(
|
|
|
439
445
|
val errorCode: String? get() = stringValue(values, "errorCode")
|
|
440
446
|
val errorMessage: String? get() = stringValue(values, "errorMessage")
|
|
441
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
|
+
}
|