@mentra/bluetooth-sdk 0.1.14 → 0.1.17
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/BluetoothSdkDefaults.swift +2 -30
- 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 +9 -1
- package/scripts/inject-ios-sdk-version.mjs +65 -0
- 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
|
@@ -42,6 +42,7 @@ private final class ActiveScanSession {
|
|
|
42
42
|
@MainActor
|
|
43
43
|
private final class PendingWifiScan {
|
|
44
44
|
let pending: PendingResponse<[WifiScanResult]>
|
|
45
|
+
var latestResults: [WifiScanResult] = []
|
|
45
46
|
|
|
46
47
|
init(pending: PendingResponse<[WifiScanResult]>) {
|
|
47
48
|
self.pending = pending
|
|
@@ -135,7 +136,7 @@ private final class PendingResponse<T> {
|
|
|
135
136
|
return
|
|
136
137
|
}
|
|
137
138
|
self?.reject(
|
|
138
|
-
|
|
139
|
+
BluetoothSdkError(
|
|
139
140
|
code: "request_timeout",
|
|
140
141
|
message: "\(self?.operation ?? "Request") timed out waiting for glasses response."
|
|
141
142
|
)
|
|
@@ -169,6 +170,7 @@ public final class MentraBluetoothSDK {
|
|
|
169
170
|
private var activeStreamKeepAlive: ActiveStreamKeepAlive?
|
|
170
171
|
private let analytics: BluetoothSdkAnalytics
|
|
171
172
|
private var pendingPhotoRequests: [String: PendingResponse<PhotoResponseEvent>] = [:]
|
|
173
|
+
private var pendingCameraStatusRequests: [String: PendingResponse<CameraStatusEvent>] = [:]
|
|
172
174
|
private var pendingVideoRecordingRequests: [String: PendingVideoRecordingRequest] = [:]
|
|
173
175
|
private var pendingRgbLedRequests: [String: PendingResponse<RgbLedControlResponseEvent>] = [:]
|
|
174
176
|
private var pendingSettingsRequests: [String: PendingResponse<SettingsAckEvent>] = [:]
|
|
@@ -235,7 +237,7 @@ public final class MentraBluetoothSDK {
|
|
|
235
237
|
|
|
236
238
|
private func requireGlassesConnected(operation: String) throws {
|
|
237
239
|
guard glassesStatus.connected else {
|
|
238
|
-
throw
|
|
240
|
+
throw BluetoothSdkError(
|
|
239
241
|
code: "glasses_not_connected",
|
|
240
242
|
message: "Cannot \(operation) because glasses are not connected."
|
|
241
243
|
)
|
|
@@ -349,7 +351,7 @@ public final class MentraBluetoothSDK {
|
|
|
349
351
|
public func connectDefault(options: ConnectOptions = ConnectOptions()) throws {
|
|
350
352
|
clearBluetoothRestoreIntent()
|
|
351
353
|
guard let device = currentDefaultDevice() else {
|
|
352
|
-
throw
|
|
354
|
+
throw BluetoothSdkError(
|
|
353
355
|
code: "default_device_missing",
|
|
354
356
|
message: "Set a default glasses device before calling connectDefault."
|
|
355
357
|
)
|
|
@@ -594,6 +596,20 @@ public final class MentraBluetoothSDK {
|
|
|
594
596
|
return result
|
|
595
597
|
}
|
|
596
598
|
|
|
599
|
+
public func setCameraTuningConfig(anrOn: Bool, gainOn: Bool) async throws -> SettingsAckEvent {
|
|
600
|
+
return try await performSettingsCommand(
|
|
601
|
+
setting: "camera_tuning",
|
|
602
|
+
updateStore: { _ in },
|
|
603
|
+
send: { requestId in
|
|
604
|
+
try DeviceManager.shared.sendCameraTuningConfig(
|
|
605
|
+
requestId: requestId,
|
|
606
|
+
anrOn: anrOn,
|
|
607
|
+
gainOn: gainOn
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
|
|
597
613
|
public func setMicState(
|
|
598
614
|
enabled: Bool,
|
|
599
615
|
useGlassesMic: Bool = true,
|
|
@@ -639,7 +655,7 @@ public final class MentraBluetoothSDK {
|
|
|
639
655
|
|
|
640
656
|
public func setGlassesMediaVolume(_ level: Int) async throws -> GlassesMediaVolumeSetResult {
|
|
641
657
|
guard (0 ... 15).contains(level) else {
|
|
642
|
-
throw
|
|
658
|
+
throw BluetoothSdkError(
|
|
643
659
|
code: "invalid_volume_level",
|
|
644
660
|
message: "Glasses media volume must be between 0 and 15."
|
|
645
661
|
)
|
|
@@ -649,13 +665,14 @@ public final class MentraBluetoothSDK {
|
|
|
649
665
|
|
|
650
666
|
public func requestWifiScan() async throws -> [WifiScanResult] {
|
|
651
667
|
guard pendingWifiScan == nil else {
|
|
652
|
-
throw
|
|
668
|
+
throw BluetoothSdkError(
|
|
653
669
|
code: "request_in_flight",
|
|
654
670
|
message: "A WiFi scan is already waiting for a glasses response."
|
|
655
671
|
)
|
|
656
672
|
}
|
|
657
673
|
let pending = PendingResponse<[WifiScanResult]>(operation: "WiFi scan request")
|
|
658
|
-
|
|
674
|
+
let request = PendingWifiScan(pending: pending)
|
|
675
|
+
pendingWifiScan = request
|
|
659
676
|
DeviceManager.shared.requestWifiScan()
|
|
660
677
|
do {
|
|
661
678
|
let results = try await pending.wait()
|
|
@@ -664,16 +681,25 @@ public final class MentraBluetoothSDK {
|
|
|
664
681
|
}
|
|
665
682
|
return results
|
|
666
683
|
} catch {
|
|
684
|
+
let fallbackResults: [WifiScanResult]
|
|
685
|
+
if (error as? BluetoothSdkError)?.code == "request_timeout" {
|
|
686
|
+
fallbackResults = request.latestResults
|
|
687
|
+
} else {
|
|
688
|
+
fallbackResults = []
|
|
689
|
+
}
|
|
667
690
|
if pendingWifiScan?.pending === pending {
|
|
668
691
|
pendingWifiScan = nil
|
|
669
692
|
}
|
|
693
|
+
if !fallbackResults.isEmpty {
|
|
694
|
+
return fallbackResults
|
|
695
|
+
}
|
|
670
696
|
throw error
|
|
671
697
|
}
|
|
672
698
|
}
|
|
673
699
|
|
|
674
700
|
public func sendWifiCredentials(ssid: String, password: String) async throws -> WifiStatusEvent {
|
|
675
701
|
guard pendingWifiStatus == nil else {
|
|
676
|
-
throw
|
|
702
|
+
throw BluetoothSdkError(
|
|
677
703
|
code: "request_in_flight",
|
|
678
704
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
679
705
|
)
|
|
@@ -697,7 +723,7 @@ public final class MentraBluetoothSDK {
|
|
|
697
723
|
|
|
698
724
|
public func forgetWifiNetwork(ssid: String) async throws -> WifiStatusEvent {
|
|
699
725
|
guard pendingWifiStatus == nil else {
|
|
700
|
-
throw
|
|
726
|
+
throw BluetoothSdkError(
|
|
701
727
|
code: "request_in_flight",
|
|
702
728
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
703
729
|
)
|
|
@@ -721,7 +747,7 @@ public final class MentraBluetoothSDK {
|
|
|
721
747
|
|
|
722
748
|
public func setHotspotState(enabled: Bool) async throws -> HotspotStatusEvent {
|
|
723
749
|
guard pendingHotspotStatus == nil else {
|
|
724
|
-
throw
|
|
750
|
+
throw BluetoothSdkError(
|
|
725
751
|
code: "request_in_flight",
|
|
726
752
|
message: "A hotspot command is already waiting for a glasses response."
|
|
727
753
|
)
|
|
@@ -750,25 +776,53 @@ public final class MentraBluetoothSDK {
|
|
|
750
776
|
}
|
|
751
777
|
|
|
752
778
|
public func requestPhoto(_ request: PhotoRequest) async throws -> PhotoResponseEvent {
|
|
779
|
+
let routedRequest = nonBlankRequestId(request.requestId).map { request.withRequestId($0) }
|
|
780
|
+
?? request.withRequestId(generatedCameraRequestId("photo"))
|
|
753
781
|
Bridge.log(
|
|
754
|
-
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=\(
|
|
782
|
+
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=\(routedRequest.requestId)"
|
|
755
783
|
)
|
|
756
|
-
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(
|
|
757
|
-
pendingPhotoRequests[
|
|
758
|
-
DeviceManager.shared.requestPhoto(
|
|
784
|
+
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(routedRequest.requestId)")
|
|
785
|
+
pendingPhotoRequests[routedRequest.requestId] = pending
|
|
786
|
+
DeviceManager.shared.requestPhoto(routedRequest)
|
|
787
|
+
do {
|
|
788
|
+
let event = try await pending.wait()
|
|
789
|
+
pendingPhotoRequests.removeValue(forKey: routedRequest.requestId)
|
|
790
|
+
return event
|
|
791
|
+
} catch {
|
|
792
|
+
pendingPhotoRequests.removeValue(forKey: routedRequest.requestId)
|
|
793
|
+
throw error
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
public func warmUpCamera(
|
|
798
|
+
requestId: String? = nil,
|
|
799
|
+
size: PhotoSize,
|
|
800
|
+
exposureTimeNs: Double?,
|
|
801
|
+
durationMs: Int
|
|
802
|
+
) async throws -> CameraStatusEvent {
|
|
803
|
+
let effectiveRequestId = nonBlankRequestId(requestId) ?? generatedCameraRequestId("warm")
|
|
804
|
+
let pending = PendingResponse<CameraStatusEvent>(operation: "camera warm up \(effectiveRequestId)")
|
|
805
|
+
pendingCameraStatusRequests[effectiveRequestId] = pending
|
|
759
806
|
do {
|
|
807
|
+
// Inside the catch so an unsupported-device throw also clears the pending entry.
|
|
808
|
+
try DeviceManager.shared.warmUpCamera(
|
|
809
|
+
requestId: effectiveRequestId,
|
|
810
|
+
size: size,
|
|
811
|
+
exposureTimeNs: exposureTimeNs,
|
|
812
|
+
durationMs: durationMs
|
|
813
|
+
)
|
|
760
814
|
let event = try await pending.wait()
|
|
761
|
-
|
|
815
|
+
pendingCameraStatusRequests.removeValue(forKey: effectiveRequestId)
|
|
762
816
|
return event
|
|
763
817
|
} catch {
|
|
764
|
-
|
|
818
|
+
pendingCameraStatusRequests.removeValue(forKey: effectiveRequestId)
|
|
765
819
|
throw error
|
|
766
820
|
}
|
|
767
821
|
}
|
|
768
822
|
|
|
769
823
|
public func queryGalleryStatus() async throws -> GalleryStatusEvent {
|
|
770
824
|
if pendingGalleryStatus != nil {
|
|
771
|
-
throw
|
|
825
|
+
throw BluetoothSdkError(
|
|
772
826
|
code: "request_in_flight",
|
|
773
827
|
message: "A gallery status query is already waiting for a glasses response."
|
|
774
828
|
)
|
|
@@ -850,7 +904,7 @@ public final class MentraBluetoothSDK {
|
|
|
850
904
|
|
|
851
905
|
public func stopStream() async throws -> StreamStatusEvent {
|
|
852
906
|
guard pendingStreamStop == nil else {
|
|
853
|
-
throw
|
|
907
|
+
throw BluetoothSdkError(
|
|
854
908
|
code: "request_in_flight",
|
|
855
909
|
message: "A stream stop command is already waiting for a glasses response."
|
|
856
910
|
)
|
|
@@ -875,14 +929,14 @@ public final class MentraBluetoothSDK {
|
|
|
875
929
|
|
|
876
930
|
public func startVideoRecording(_ request: VideoRecordingRequest) async throws -> VideoRecordingStatusEvent {
|
|
877
931
|
guard !request.requestId.isEmpty else {
|
|
878
|
-
throw
|
|
932
|
+
throw BluetoothSdkError(code: "missing_request_id", message: "requestId is required to start video recording.")
|
|
879
933
|
}
|
|
880
934
|
try requireGlassesConnected(operation: "start video recording")
|
|
881
935
|
let pending = PendingResponse<VideoRecordingStatusEvent>(
|
|
882
936
|
operation: "start video recording \(request.requestId)"
|
|
883
937
|
)
|
|
884
938
|
guard pendingVideoRecordingRequests[request.requestId] == nil else {
|
|
885
|
-
throw
|
|
939
|
+
throw BluetoothSdkError(
|
|
886
940
|
code: "request_in_flight",
|
|
887
941
|
message: "A video recording command is already waiting for requestId \(request.requestId)."
|
|
888
942
|
)
|
|
@@ -914,12 +968,12 @@ public final class MentraBluetoothSDK {
|
|
|
914
968
|
requestId: String, webhookUrl: String? = nil, authToken: String? = nil
|
|
915
969
|
) async throws -> VideoRecordingStatusEvent {
|
|
916
970
|
guard !requestId.isEmpty else {
|
|
917
|
-
throw
|
|
971
|
+
throw BluetoothSdkError(code: "missing_request_id", message: "requestId is required to stop video recording.")
|
|
918
972
|
}
|
|
919
973
|
try requireGlassesConnected(operation: "stop video recording")
|
|
920
974
|
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "stop video recording \(requestId)")
|
|
921
975
|
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
922
|
-
throw
|
|
976
|
+
throw BluetoothSdkError(
|
|
923
977
|
code: "request_in_flight",
|
|
924
978
|
message: "A video recording command is already waiting for requestId \(requestId)."
|
|
925
979
|
)
|
|
@@ -944,7 +998,7 @@ public final class MentraBluetoothSDK {
|
|
|
944
998
|
|
|
945
999
|
public func requestVersionInfo() async throws -> VersionInfoResult {
|
|
946
1000
|
guard pendingVersionInfo == nil else {
|
|
947
|
-
throw
|
|
1001
|
+
throw BluetoothSdkError(
|
|
948
1002
|
code: "request_in_flight",
|
|
949
1003
|
message: "A version info request is already waiting for a glasses response."
|
|
950
1004
|
)
|
|
@@ -978,13 +1032,13 @@ public final class MentraBluetoothSDK {
|
|
|
978
1032
|
public func checkForOtaUpdate() async throws -> Bool {
|
|
979
1033
|
let status = await getFreshGlassesStatus()
|
|
980
1034
|
guard status.connected else {
|
|
981
|
-
throw
|
|
1035
|
+
throw BluetoothSdkError(
|
|
982
1036
|
code: "glasses_not_connected",
|
|
983
1037
|
message: "Cannot check OTA update because glasses are not connected."
|
|
984
1038
|
)
|
|
985
1039
|
}
|
|
986
1040
|
guard !status.buildNumber.isEmpty else {
|
|
987
|
-
throw
|
|
1041
|
+
throw BluetoothSdkError(
|
|
988
1042
|
code: "missing_glasses_version",
|
|
989
1043
|
message: "Cannot check OTA update because glasses build number is unavailable."
|
|
990
1044
|
)
|
|
@@ -1013,7 +1067,7 @@ public final class MentraBluetoothSDK {
|
|
|
1013
1067
|
sendRequest: () -> Void
|
|
1014
1068
|
) async throws -> OtaQueryResult {
|
|
1015
1069
|
if pendingOtaQuery != nil {
|
|
1016
|
-
throw
|
|
1070
|
+
throw BluetoothSdkError(
|
|
1017
1071
|
code: "request_in_flight",
|
|
1018
1072
|
message: "An OTA status query is already waiting for a glasses response."
|
|
1019
1073
|
)
|
|
@@ -1044,7 +1098,7 @@ public final class MentraBluetoothSDK {
|
|
|
1044
1098
|
|
|
1045
1099
|
private func startOtaCommand(otaVersionUrl: String) async throws -> OtaStartAckEvent {
|
|
1046
1100
|
if pendingOtaStart != nil {
|
|
1047
|
-
throw
|
|
1101
|
+
throw BluetoothSdkError(
|
|
1048
1102
|
code: "request_in_flight",
|
|
1049
1103
|
message: "An OTA start command is already waiting for a glasses response."
|
|
1050
1104
|
)
|
|
@@ -1072,13 +1126,6 @@ public final class MentraBluetoothSDK {
|
|
|
1072
1126
|
|
|
1073
1127
|
func sendOtaQueryStatus() async throws -> OtaQueryResult { try await queryOtaStatus() }
|
|
1074
1128
|
|
|
1075
|
-
/// Re-run the glasses-side OTA version check after an internal clock-skew recovery.
|
|
1076
|
-
func retryOtaVersionCheck() async throws -> OtaQueryResult {
|
|
1077
|
-
try await performOtaQuery(operation: "OTA version retry") {
|
|
1078
|
-
DeviceManager.shared.retryOtaVersionCheck()
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
1129
|
private func getFreshGlassesStatus() async -> GlassesStatus {
|
|
1083
1130
|
let status = glassesStatus
|
|
1084
1131
|
if !status.connected || !status.buildNumber.isEmpty {
|
|
@@ -1114,7 +1161,7 @@ public final class MentraBluetoothSDK {
|
|
|
1114
1161
|
}
|
|
1115
1162
|
|
|
1116
1163
|
guard status.connected else {
|
|
1117
|
-
throw
|
|
1164
|
+
throw BluetoothSdkError(
|
|
1118
1165
|
code: "glasses_not_connected",
|
|
1119
1166
|
message: "Cannot check OTA update because glasses disconnected."
|
|
1120
1167
|
)
|
|
@@ -1148,7 +1195,7 @@ public final class MentraBluetoothSDK {
|
|
|
1148
1195
|
private func resolveOtaVersionUrl(status: GlassesStatus) throws -> String {
|
|
1149
1196
|
let deviceUrl = status.otaVersionUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1150
1197
|
if isLegacyAsgOtaStartBuild(status.buildNumber) {
|
|
1151
|
-
return
|
|
1198
|
+
return OtaManifestDefaults.legacyProdOtaVersionUrl
|
|
1152
1199
|
}
|
|
1153
1200
|
// SDK consumers are pinned to the manifest built for their SDK version.
|
|
1154
1201
|
// A future glasses-advertised URL should not silently change that pairing.
|
|
@@ -1160,7 +1207,7 @@ public final class MentraBluetoothSDK {
|
|
|
1160
1207
|
|
|
1161
1208
|
private func isLegacyAsgOtaStartBuild(_ buildNumber: String) -> Bool {
|
|
1162
1209
|
guard let parsed = Int(buildNumber) else { return false }
|
|
1163
|
-
return parsed <
|
|
1210
|
+
return parsed < 39
|
|
1164
1211
|
}
|
|
1165
1212
|
|
|
1166
1213
|
func sendShutdown() {
|
|
@@ -1420,14 +1467,14 @@ public final class MentraBluetoothSDK {
|
|
|
1420
1467
|
)
|
|
1421
1468
|
}
|
|
1422
1469
|
|
|
1423
|
-
private func streamStatusError(_ event: StreamStatusEvent, code: String) ->
|
|
1470
|
+
private func streamStatusError(_ event: StreamStatusEvent, code: String) -> BluetoothSdkError {
|
|
1424
1471
|
let message: String
|
|
1425
1472
|
if case let .error(_, errorDetails, _, _) = event.status {
|
|
1426
1473
|
message = errorDetails
|
|
1427
1474
|
} else {
|
|
1428
1475
|
message = "Stream status \(event.state.rawValue)"
|
|
1429
1476
|
}
|
|
1430
|
-
return
|
|
1477
|
+
return BluetoothSdkError(code: code, message: message)
|
|
1431
1478
|
}
|
|
1432
1479
|
|
|
1433
1480
|
private func handlePhotoResponseForRequests(_ event: PhotoResponseEvent) {
|
|
@@ -1437,7 +1484,7 @@ public final class MentraBluetoothSDK {
|
|
|
1437
1484
|
pending.resolve(event)
|
|
1438
1485
|
case let .error(_, errorCode, errorMessage, _):
|
|
1439
1486
|
pending.reject(
|
|
1440
|
-
|
|
1487
|
+
BluetoothSdkError(
|
|
1441
1488
|
code: errorCode ?? "photo_request_failed",
|
|
1442
1489
|
message: errorMessage
|
|
1443
1490
|
)
|
|
@@ -1445,6 +1492,24 @@ public final class MentraBluetoothSDK {
|
|
|
1445
1492
|
}
|
|
1446
1493
|
}
|
|
1447
1494
|
|
|
1495
|
+
private func handleCameraStatusForRequests(_ event: CameraStatusEvent) {
|
|
1496
|
+
guard let pending = pendingCameraStatusRequests[event.requestId] else { return }
|
|
1497
|
+
switch event.state.lowercased() {
|
|
1498
|
+
case "ready":
|
|
1499
|
+
pending.resolve(event)
|
|
1500
|
+
case "error":
|
|
1501
|
+
pending.reject(
|
|
1502
|
+
BluetoothSdkError(
|
|
1503
|
+
code: event.errorCode ?? "camera_warm_up_failed",
|
|
1504
|
+
message: event.errorMessage ?? "Camera warm-up failed."
|
|
1505
|
+
)
|
|
1506
|
+
)
|
|
1507
|
+
default:
|
|
1508
|
+
// "warming"/"stopped" are progress updates; leave the pending promise alone.
|
|
1509
|
+
break
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1448
1513
|
private func handleVideoRecordingStatusForRequests(_ event: VideoRecordingStatusEvent) {
|
|
1449
1514
|
guard let request = pendingVideoRecordingRequests[event.requestId] else { return }
|
|
1450
1515
|
if event.success {
|
|
@@ -1460,7 +1525,7 @@ public final class MentraBluetoothSDK {
|
|
|
1460
1525
|
}
|
|
1461
1526
|
} else {
|
|
1462
1527
|
request.pending.reject(
|
|
1463
|
-
|
|
1528
|
+
BluetoothSdkError(
|
|
1464
1529
|
code: event.status.isEmpty ? "video_recording_failed" : event.status,
|
|
1465
1530
|
message: event.details ?? "Video recording command failed."
|
|
1466
1531
|
)
|
|
@@ -1480,7 +1545,7 @@ public final class MentraBluetoothSDK {
|
|
|
1480
1545
|
}
|
|
1481
1546
|
} else {
|
|
1482
1547
|
request.pending.reject(
|
|
1483
|
-
|
|
1548
|
+
BluetoothSdkError(
|
|
1484
1549
|
code: "video_upload_failed",
|
|
1485
1550
|
message: event.errorMessage ?? "Video upload failed."
|
|
1486
1551
|
)
|
|
@@ -1494,7 +1559,7 @@ public final class MentraBluetoothSDK {
|
|
|
1494
1559
|
pending.resolve(event)
|
|
1495
1560
|
} else {
|
|
1496
1561
|
pending.reject(
|
|
1497
|
-
|
|
1562
|
+
BluetoothSdkError(
|
|
1498
1563
|
code: event.errorCode ?? "rgb_led_control_failed",
|
|
1499
1564
|
message: event.errorCode ?? "RGB LED command failed."
|
|
1500
1565
|
)
|
|
@@ -1507,7 +1572,7 @@ public final class MentraBluetoothSDK {
|
|
|
1507
1572
|
if isFailureStatus(event.status) {
|
|
1508
1573
|
let fallbackSetting = event.setting.isEmpty ? event.requestId : event.setting
|
|
1509
1574
|
pending.reject(
|
|
1510
|
-
|
|
1575
|
+
BluetoothSdkError(
|
|
1511
1576
|
code: event.errorCode ?? "\(event.setting.isEmpty ? "settings" : event.setting)_failed",
|
|
1512
1577
|
message: event.errorMessage ?? "Settings command \(fallbackSetting) failed."
|
|
1513
1578
|
)
|
|
@@ -1529,6 +1594,11 @@ public final class MentraBluetoothSDK {
|
|
|
1529
1594
|
request.pending.resolve(results)
|
|
1530
1595
|
}
|
|
1531
1596
|
|
|
1597
|
+
private func updateWifiScanLatestResults(_ results: [WifiScanResult]) {
|
|
1598
|
+
guard !results.isEmpty else { return }
|
|
1599
|
+
pendingWifiScan?.latestResults = results
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1532
1602
|
private func handleWifiStatusForRequests(_ event: WifiStatusEvent) {
|
|
1533
1603
|
guard let request = pendingWifiStatus else { return }
|
|
1534
1604
|
guard wifiStatusMatches(event.status, request: request) else { return }
|
|
@@ -1577,7 +1647,7 @@ public final class MentraBluetoothSDK {
|
|
|
1577
1647
|
pendingHotspotStatus = nil
|
|
1578
1648
|
}
|
|
1579
1649
|
request.pending.reject(
|
|
1580
|
-
|
|
1650
|
+
BluetoothSdkError(
|
|
1581
1651
|
code: "hotspot_command_failed",
|
|
1582
1652
|
message: event.message ?? "Hotspot command failed."
|
|
1583
1653
|
)
|
|
@@ -1717,6 +1787,7 @@ public final class MentraBluetoothSDK {
|
|
|
1717
1787
|
let networks = (data["networks"] as? [[String: Any]])?.map(WifiScanResult.init(values:)) ?? []
|
|
1718
1788
|
let hasCompletionFlag = data.keys.contains("scanComplete") || data.keys.contains("scan_complete")
|
|
1719
1789
|
let scanComplete = data["scanComplete"] as? Bool ?? data["scan_complete"] as? Bool ?? false
|
|
1790
|
+
updateWifiScanLatestResults(networks)
|
|
1720
1791
|
if scanComplete || !hasCompletionFlag {
|
|
1721
1792
|
handleWifiScanResultsForRequests(networks)
|
|
1722
1793
|
}
|
|
@@ -1735,6 +1806,10 @@ public final class MentraBluetoothSDK {
|
|
|
1735
1806
|
delegate?.mentraBluetoothSDK(self, didReceive: .photoResponse(event))
|
|
1736
1807
|
case "photo_status":
|
|
1737
1808
|
delegate?.mentraBluetoothSDK(self, didReceive: .photoStatus(PhotoStatusEvent(values: data)))
|
|
1809
|
+
case "camera_status":
|
|
1810
|
+
let event = CameraStatusEvent(values: data)
|
|
1811
|
+
handleCameraStatusForRequests(event)
|
|
1812
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .cameraStatus(event))
|
|
1738
1813
|
case "video_recording_status":
|
|
1739
1814
|
let event = VideoRecordingStatusEvent(values: data)
|
|
1740
1815
|
handleVideoRecordingStatusForRequests(event)
|
|
@@ -1757,11 +1832,6 @@ public final class MentraBluetoothSDK {
|
|
|
1757
1832
|
if !handleStreamKeepAliveAck(event) {
|
|
1758
1833
|
delegate?.mentraBluetoothSDK(self, didReceive: .keepAliveAck(event))
|
|
1759
1834
|
}
|
|
1760
|
-
case "ota_update_available":
|
|
1761
|
-
var resultValues = data
|
|
1762
|
-
resultValues["type"] = "ota_update_available"
|
|
1763
|
-
pendingOtaQuery?.resolve(OtaQueryResult(values: resultValues))
|
|
1764
|
-
delegate?.mentraBluetoothSDK(self, didReceive: .otaUpdateAvailable(OtaUpdateAvailableEvent(values: resultValues)))
|
|
1765
1835
|
case "ota_start_ack":
|
|
1766
1836
|
var values = data
|
|
1767
1837
|
values["type"] = "ota_start_ack"
|
|
@@ -1786,7 +1856,7 @@ public final class MentraBluetoothSDK {
|
|
|
1786
1856
|
case "pair_failure":
|
|
1787
1857
|
delegate?.mentraBluetoothSDK(
|
|
1788
1858
|
self,
|
|
1789
|
-
didFail:
|
|
1859
|
+
didFail: BluetoothSdkError(
|
|
1790
1860
|
code: "pair_failure",
|
|
1791
1861
|
message: data["error"] as? String ?? data.description
|
|
1792
1862
|
)
|
|
@@ -2,15 +2,15 @@ import Foundation
|
|
|
2
2
|
|
|
3
3
|
enum OtaManifestDefaults {
|
|
4
4
|
private static let sdkOtaReleaseBaseUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/bluetooth-sdk-ota"
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
static let
|
|
5
|
+
// ASG builds before 39 ignore ota_start.ota_version_url, so SDK checks must
|
|
6
|
+
// use the same legacy production manifest those glasses will install from.
|
|
7
|
+
static let legacyProdOtaVersionUrl = "https://ota.mentraglass.com/prod_live_version.json"
|
|
8
8
|
|
|
9
9
|
static func defaultOtaVersionUrl() throws -> String {
|
|
10
10
|
guard let sdkVersion = BluetoothSdkDefaults.sdkVersion,
|
|
11
11
|
!sdkVersion.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
12
12
|
else {
|
|
13
|
-
throw
|
|
13
|
+
throw BluetoothSdkError(
|
|
14
14
|
code: "missing_sdk_version",
|
|
15
15
|
message: "Cannot determine Bluetooth SDK version for the default OTA manifest URL."
|
|
16
16
|
)
|
|
@@ -55,28 +55,28 @@ enum OtaManifestChecker {
|
|
|
55
55
|
static func normalizeHttpUrl(_ value: String) throws -> String {
|
|
56
56
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
57
57
|
guard !trimmed.isEmpty else {
|
|
58
|
-
throw
|
|
58
|
+
throw BluetoothSdkError(code: "invalid_ota_url", message: "OTA version URL must be a non-empty http(s) URL.")
|
|
59
59
|
}
|
|
60
60
|
guard let url = URL(string: trimmed),
|
|
61
61
|
let scheme = url.scheme?.lowercased(),
|
|
62
62
|
(scheme == "http" || scheme == "https"),
|
|
63
63
|
url.host?.isEmpty == false
|
|
64
64
|
else {
|
|
65
|
-
throw
|
|
65
|
+
throw BluetoothSdkError(code: "invalid_ota_url", message: "OTA version URL must be a valid http(s) URL.")
|
|
66
66
|
}
|
|
67
67
|
return url.absoluteString
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
static func fetch(_ otaVersionUrl: String) async throws -> OtaManifest {
|
|
71
71
|
guard let url = URL(string: otaVersionUrl) else {
|
|
72
|
-
throw
|
|
72
|
+
throw BluetoothSdkError(code: "invalid_ota_url", message: "OTA version URL must be a valid http(s) URL.")
|
|
73
73
|
}
|
|
74
74
|
let (data, response) = try await URLSession.shared.data(from: url)
|
|
75
75
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
76
|
-
throw
|
|
76
|
+
throw BluetoothSdkError(code: "ota_manifest_request_failed", message: "OTA manifest request failed.")
|
|
77
77
|
}
|
|
78
78
|
guard (200 ... 299).contains(httpResponse.statusCode) else {
|
|
79
|
-
throw
|
|
79
|
+
throw BluetoothSdkError(
|
|
80
80
|
code: "ota_manifest_request_failed",
|
|
81
81
|
message: "OTA manifest request failed with HTTP \(httpResponse.statusCode)."
|
|
82
82
|
)
|
|
@@ -110,18 +110,18 @@ enum OtaManifestChecker {
|
|
|
110
110
|
if let versionCode = manifest.versionCode {
|
|
111
111
|
return OtaManifestApp(versionCode: versionCode)
|
|
112
112
|
}
|
|
113
|
-
throw
|
|
113
|
+
throw BluetoothSdkError(code: "invalid_ota_manifest", message: "OTA manifest is missing ASG app versionCode.")
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
private static func hasApkUpdate(currentBuildNumber: String, manifest: OtaManifest) throws -> Bool {
|
|
117
117
|
guard let currentVersion = Int(currentBuildNumber) else {
|
|
118
|
-
throw
|
|
118
|
+
throw BluetoothSdkError(
|
|
119
119
|
code: "invalid_glasses_version",
|
|
120
120
|
message: "Cannot check OTA update because glasses build number is invalid."
|
|
121
121
|
)
|
|
122
122
|
}
|
|
123
123
|
guard let serverVersion = try latestAppInfo(manifest).versionCode else {
|
|
124
|
-
throw
|
|
124
|
+
throw BluetoothSdkError(code: "invalid_ota_manifest", message: "OTA manifest is missing ASG app versionCode.")
|
|
125
125
|
}
|
|
126
126
|
return serverVersion > currentVersion
|
|
127
127
|
}
|
|
@@ -144,7 +144,7 @@ enum OtaManifestChecker {
|
|
|
144
144
|
private static func hasBesUpdate(besFirmware: BesFirmware?, currentVersion: String) throws -> Bool {
|
|
145
145
|
guard let besFirmware else { return false }
|
|
146
146
|
guard let serverVersion = besFirmware.version, !serverVersion.isEmpty else {
|
|
147
|
-
throw
|
|
147
|
+
throw BluetoothSdkError(code: "invalid_ota_manifest", message: "OTA manifest bes_firmware.version is missing.")
|
|
148
148
|
}
|
|
149
149
|
if currentVersion.isEmpty {
|
|
150
150
|
return true
|