@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
|
@@ -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
|
)
|
|
@@ -150,6 +151,7 @@ public final class MentraBluetoothSDK {
|
|
|
150
151
|
private static let otaBesVersionWaitMs = 5_000
|
|
151
152
|
private static let otaMtkVersionWaitMs = 2_000
|
|
152
153
|
private static let otaVersionPollMs = 100
|
|
154
|
+
private static let defaultStreamKeepAliveIntervalSeconds = 5
|
|
153
155
|
|
|
154
156
|
public weak var delegate: MentraBluetoothSDKDelegate?
|
|
155
157
|
|
|
@@ -168,6 +170,7 @@ public final class MentraBluetoothSDK {
|
|
|
168
170
|
private var activeStreamKeepAlive: ActiveStreamKeepAlive?
|
|
169
171
|
private let analytics: BluetoothSdkAnalytics
|
|
170
172
|
private var pendingPhotoRequests: [String: PendingResponse<PhotoResponseEvent>] = [:]
|
|
173
|
+
private var pendingCameraStatusRequests: [String: PendingResponse<CameraStatusEvent>] = [:]
|
|
171
174
|
private var pendingVideoRecordingRequests: [String: PendingVideoRecordingRequest] = [:]
|
|
172
175
|
private var pendingRgbLedRequests: [String: PendingResponse<RgbLedControlResponseEvent>] = [:]
|
|
173
176
|
private var pendingSettingsRequests: [String: PendingResponse<SettingsAckEvent>] = [:]
|
|
@@ -234,7 +237,7 @@ public final class MentraBluetoothSDK {
|
|
|
234
237
|
|
|
235
238
|
private func requireGlassesConnected(operation: String) throws {
|
|
236
239
|
guard glassesStatus.connected else {
|
|
237
|
-
throw
|
|
240
|
+
throw BluetoothSdkError(
|
|
238
241
|
code: "glasses_not_connected",
|
|
239
242
|
message: "Cannot \(operation) because glasses are not connected."
|
|
240
243
|
)
|
|
@@ -348,7 +351,7 @@ public final class MentraBluetoothSDK {
|
|
|
348
351
|
public func connectDefault(options: ConnectOptions = ConnectOptions()) throws {
|
|
349
352
|
clearBluetoothRestoreIntent()
|
|
350
353
|
guard let device = currentDefaultDevice() else {
|
|
351
|
-
throw
|
|
354
|
+
throw BluetoothSdkError(
|
|
352
355
|
code: "default_device_missing",
|
|
353
356
|
message: "Set a default glasses device before calling connectDefault."
|
|
354
357
|
)
|
|
@@ -482,11 +485,7 @@ public final class MentraBluetoothSDK {
|
|
|
482
485
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "voice_activity_detection_enabled", enabled)
|
|
483
486
|
}
|
|
484
487
|
|
|
485
|
-
public func
|
|
486
|
-
try await setButtonPhotoSettings(ButtonPhotoSettings(size: size))
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
public func setButtonPhotoSettings(_ settings: ButtonPhotoSettings) async throws -> SettingsAckEvent {
|
|
488
|
+
public func setPhotoCaptureDefaults(_ settings: PhotoCaptureDefaults) async throws -> SettingsAckEvent {
|
|
490
489
|
try await performSettingsCommand(
|
|
491
490
|
setting: "button_photo",
|
|
492
491
|
updateStore: { _ in
|
|
@@ -545,38 +544,26 @@ public final class MentraBluetoothSDK {
|
|
|
545
544
|
)
|
|
546
545
|
}
|
|
547
546
|
|
|
548
|
-
public func
|
|
547
|
+
public func setVideoRecordingDefaults(_ defaults: VideoRecordingDefaults) async throws -> SettingsAckEvent {
|
|
549
548
|
try await performSettingsCommand(
|
|
550
549
|
setting: "button_video_recording",
|
|
551
550
|
updateStore: { _ in
|
|
552
|
-
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_width", width)
|
|
553
|
-
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_height", height)
|
|
554
|
-
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_fps", fps)
|
|
551
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_width", defaults.width)
|
|
552
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_height", defaults.height)
|
|
553
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_fps", defaults.fps)
|
|
555
554
|
},
|
|
556
555
|
send: { requestId in
|
|
557
556
|
try DeviceManager.shared.sendButtonVideoRecordingSettings(
|
|
558
557
|
requestId: requestId,
|
|
559
|
-
width: width,
|
|
560
|
-
height: height,
|
|
561
|
-
fps: fps
|
|
558
|
+
width: defaults.width,
|
|
559
|
+
height: defaults.height,
|
|
560
|
+
fps: defaults.fps
|
|
562
561
|
)
|
|
563
562
|
}
|
|
564
563
|
)
|
|
565
564
|
}
|
|
566
565
|
|
|
567
|
-
public func
|
|
568
|
-
try await setButtonVideoRecordingSettings(width: settings.width, height: settings.height, fps: settings.fps)
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
public func setButtonCameraLed(enabled: Bool) async throws -> SettingsAckEvent {
|
|
572
|
-
try await performSettingsCommand(
|
|
573
|
-
setting: "button_camera_led",
|
|
574
|
-
updateStore: { _ in DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_camera_led", enabled) },
|
|
575
|
-
send: { requestId in try DeviceManager.shared.sendButtonCameraLedSetting(requestId: requestId, enabled: enabled) }
|
|
576
|
-
)
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
public func setButtonMaxRecordingTime(minutes: Int) async throws -> SettingsAckEvent {
|
|
566
|
+
public func setMaxVideoRecordingDuration(minutes: Int) async throws -> SettingsAckEvent {
|
|
580
567
|
try await performSettingsCommand(
|
|
581
568
|
setting: "button_max_recording_time",
|
|
582
569
|
updateStore: { _ in
|
|
@@ -609,6 +596,20 @@ public final class MentraBluetoothSDK {
|
|
|
609
596
|
return result
|
|
610
597
|
}
|
|
611
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
|
+
|
|
612
613
|
public func setMicState(
|
|
613
614
|
enabled: Bool,
|
|
614
615
|
useGlassesMic: Bool = true,
|
|
@@ -654,7 +655,7 @@ public final class MentraBluetoothSDK {
|
|
|
654
655
|
|
|
655
656
|
public func setGlassesMediaVolume(_ level: Int) async throws -> GlassesMediaVolumeSetResult {
|
|
656
657
|
guard (0 ... 15).contains(level) else {
|
|
657
|
-
throw
|
|
658
|
+
throw BluetoothSdkError(
|
|
658
659
|
code: "invalid_volume_level",
|
|
659
660
|
message: "Glasses media volume must be between 0 and 15."
|
|
660
661
|
)
|
|
@@ -664,13 +665,14 @@ public final class MentraBluetoothSDK {
|
|
|
664
665
|
|
|
665
666
|
public func requestWifiScan() async throws -> [WifiScanResult] {
|
|
666
667
|
guard pendingWifiScan == nil else {
|
|
667
|
-
throw
|
|
668
|
+
throw BluetoothSdkError(
|
|
668
669
|
code: "request_in_flight",
|
|
669
670
|
message: "A WiFi scan is already waiting for a glasses response."
|
|
670
671
|
)
|
|
671
672
|
}
|
|
672
673
|
let pending = PendingResponse<[WifiScanResult]>(operation: "WiFi scan request")
|
|
673
|
-
|
|
674
|
+
let request = PendingWifiScan(pending: pending)
|
|
675
|
+
pendingWifiScan = request
|
|
674
676
|
DeviceManager.shared.requestWifiScan()
|
|
675
677
|
do {
|
|
676
678
|
let results = try await pending.wait()
|
|
@@ -679,16 +681,25 @@ public final class MentraBluetoothSDK {
|
|
|
679
681
|
}
|
|
680
682
|
return results
|
|
681
683
|
} catch {
|
|
684
|
+
let fallbackResults: [WifiScanResult]
|
|
685
|
+
if (error as? BluetoothSdkError)?.code == "request_timeout" {
|
|
686
|
+
fallbackResults = request.latestResults
|
|
687
|
+
} else {
|
|
688
|
+
fallbackResults = []
|
|
689
|
+
}
|
|
682
690
|
if pendingWifiScan?.pending === pending {
|
|
683
691
|
pendingWifiScan = nil
|
|
684
692
|
}
|
|
693
|
+
if !fallbackResults.isEmpty {
|
|
694
|
+
return fallbackResults
|
|
695
|
+
}
|
|
685
696
|
throw error
|
|
686
697
|
}
|
|
687
698
|
}
|
|
688
699
|
|
|
689
700
|
public func sendWifiCredentials(ssid: String, password: String) async throws -> WifiStatusEvent {
|
|
690
701
|
guard pendingWifiStatus == nil else {
|
|
691
|
-
throw
|
|
702
|
+
throw BluetoothSdkError(
|
|
692
703
|
code: "request_in_flight",
|
|
693
704
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
694
705
|
)
|
|
@@ -712,7 +723,7 @@ public final class MentraBluetoothSDK {
|
|
|
712
723
|
|
|
713
724
|
public func forgetWifiNetwork(ssid: String) async throws -> WifiStatusEvent {
|
|
714
725
|
guard pendingWifiStatus == nil else {
|
|
715
|
-
throw
|
|
726
|
+
throw BluetoothSdkError(
|
|
716
727
|
code: "request_in_flight",
|
|
717
728
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
718
729
|
)
|
|
@@ -736,7 +747,7 @@ public final class MentraBluetoothSDK {
|
|
|
736
747
|
|
|
737
748
|
public func setHotspotState(enabled: Bool) async throws -> HotspotStatusEvent {
|
|
738
749
|
guard pendingHotspotStatus == nil else {
|
|
739
|
-
throw
|
|
750
|
+
throw BluetoothSdkError(
|
|
740
751
|
code: "request_in_flight",
|
|
741
752
|
message: "A hotspot command is already waiting for a glasses response."
|
|
742
753
|
)
|
|
@@ -765,25 +776,53 @@ public final class MentraBluetoothSDK {
|
|
|
765
776
|
}
|
|
766
777
|
|
|
767
778
|
public func requestPhoto(_ request: PhotoRequest) async throws -> PhotoResponseEvent {
|
|
779
|
+
let routedRequest = nonBlankRequestId(request.requestId).map { request.withRequestId($0) }
|
|
780
|
+
?? request.withRequestId(generatedCameraRequestId("photo"))
|
|
768
781
|
Bridge.log(
|
|
769
|
-
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=\(
|
|
782
|
+
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=\(routedRequest.requestId)"
|
|
770
783
|
)
|
|
771
|
-
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(
|
|
772
|
-
pendingPhotoRequests[
|
|
773
|
-
DeviceManager.shared.requestPhoto(
|
|
784
|
+
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(routedRequest.requestId)")
|
|
785
|
+
pendingPhotoRequests[routedRequest.requestId] = pending
|
|
786
|
+
DeviceManager.shared.requestPhoto(routedRequest)
|
|
774
787
|
do {
|
|
775
788
|
let event = try await pending.wait()
|
|
776
|
-
pendingPhotoRequests.removeValue(forKey:
|
|
789
|
+
pendingPhotoRequests.removeValue(forKey: routedRequest.requestId)
|
|
777
790
|
return event
|
|
778
791
|
} catch {
|
|
779
|
-
pendingPhotoRequests.removeValue(forKey:
|
|
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
|
|
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
|
+
)
|
|
814
|
+
let event = try await pending.wait()
|
|
815
|
+
pendingCameraStatusRequests.removeValue(forKey: effectiveRequestId)
|
|
816
|
+
return event
|
|
817
|
+
} catch {
|
|
818
|
+
pendingCameraStatusRequests.removeValue(forKey: effectiveRequestId)
|
|
780
819
|
throw error
|
|
781
820
|
}
|
|
782
821
|
}
|
|
783
822
|
|
|
784
823
|
public func queryGalleryStatus() async throws -> GalleryStatusEvent {
|
|
785
824
|
if pendingGalleryStatus != nil {
|
|
786
|
-
throw
|
|
825
|
+
throw BluetoothSdkError(
|
|
787
826
|
code: "request_in_flight",
|
|
788
827
|
message: "A gallery status query is already waiting for a glasses response."
|
|
789
828
|
)
|
|
@@ -806,6 +845,14 @@ public final class MentraBluetoothSDK {
|
|
|
806
845
|
}
|
|
807
846
|
|
|
808
847
|
public func startStream(_ request: StreamRequest) async throws -> StreamStatusEvent {
|
|
848
|
+
try await startStream(request, startSdkKeepAlive: true)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
func startExternallyManagedStream(_ request: StreamRequest) async throws -> StreamStatusEvent {
|
|
852
|
+
try await startStream(request, startSdkKeepAlive: false)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
private func startStream(_ request: StreamRequest, startSdkKeepAlive: Bool) async throws -> StreamStatusEvent {
|
|
809
856
|
var values = request.values
|
|
810
857
|
let streamId = stringValue(values, "streamId").flatMap { $0.isEmpty ? nil : $0 } ?? "sdk-\(UUID().uuidString)"
|
|
811
858
|
values["streamId"] = streamId
|
|
@@ -816,8 +863,11 @@ public final class MentraBluetoothSDK {
|
|
|
816
863
|
do {
|
|
817
864
|
let event = try await pending.wait(timeoutMs: 30_000)
|
|
818
865
|
pendingStreamStarts.removeValue(forKey: streamId)
|
|
819
|
-
if
|
|
820
|
-
startStreamKeepAliveMonitor(
|
|
866
|
+
if startSdkKeepAlive {
|
|
867
|
+
startStreamKeepAliveMonitor(
|
|
868
|
+
streamId: streamId,
|
|
869
|
+
intervalSeconds: Self.defaultStreamKeepAliveIntervalSeconds
|
|
870
|
+
)
|
|
821
871
|
}
|
|
822
872
|
return event
|
|
823
873
|
} catch {
|
|
@@ -854,7 +904,7 @@ public final class MentraBluetoothSDK {
|
|
|
854
904
|
|
|
855
905
|
public func stopStream() async throws -> StreamStatusEvent {
|
|
856
906
|
guard pendingStreamStop == nil else {
|
|
857
|
-
throw
|
|
907
|
+
throw BluetoothSdkError(
|
|
858
908
|
code: "request_in_flight",
|
|
859
909
|
message: "A stream stop command is already waiting for a glasses response."
|
|
860
910
|
)
|
|
@@ -879,14 +929,14 @@ public final class MentraBluetoothSDK {
|
|
|
879
929
|
|
|
880
930
|
public func startVideoRecording(_ request: VideoRecordingRequest) async throws -> VideoRecordingStatusEvent {
|
|
881
931
|
guard !request.requestId.isEmpty else {
|
|
882
|
-
throw
|
|
932
|
+
throw BluetoothSdkError(code: "missing_request_id", message: "requestId is required to start video recording.")
|
|
883
933
|
}
|
|
884
934
|
try requireGlassesConnected(operation: "start video recording")
|
|
885
935
|
let pending = PendingResponse<VideoRecordingStatusEvent>(
|
|
886
936
|
operation: "start video recording \(request.requestId)"
|
|
887
937
|
)
|
|
888
938
|
guard pendingVideoRecordingRequests[request.requestId] == nil else {
|
|
889
|
-
throw
|
|
939
|
+
throw BluetoothSdkError(
|
|
890
940
|
code: "request_in_flight",
|
|
891
941
|
message: "A video recording command is already waiting for requestId \(request.requestId)."
|
|
892
942
|
)
|
|
@@ -918,12 +968,12 @@ public final class MentraBluetoothSDK {
|
|
|
918
968
|
requestId: String, webhookUrl: String? = nil, authToken: String? = nil
|
|
919
969
|
) async throws -> VideoRecordingStatusEvent {
|
|
920
970
|
guard !requestId.isEmpty else {
|
|
921
|
-
throw
|
|
971
|
+
throw BluetoothSdkError(code: "missing_request_id", message: "requestId is required to stop video recording.")
|
|
922
972
|
}
|
|
923
973
|
try requireGlassesConnected(operation: "stop video recording")
|
|
924
974
|
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "stop video recording \(requestId)")
|
|
925
975
|
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
926
|
-
throw
|
|
976
|
+
throw BluetoothSdkError(
|
|
927
977
|
code: "request_in_flight",
|
|
928
978
|
message: "A video recording command is already waiting for requestId \(requestId)."
|
|
929
979
|
)
|
|
@@ -948,7 +998,7 @@ public final class MentraBluetoothSDK {
|
|
|
948
998
|
|
|
949
999
|
public func requestVersionInfo() async throws -> VersionInfoResult {
|
|
950
1000
|
guard pendingVersionInfo == nil else {
|
|
951
|
-
throw
|
|
1001
|
+
throw BluetoothSdkError(
|
|
952
1002
|
code: "request_in_flight",
|
|
953
1003
|
message: "A version info request is already waiting for a glasses response."
|
|
954
1004
|
)
|
|
@@ -982,13 +1032,13 @@ public final class MentraBluetoothSDK {
|
|
|
982
1032
|
public func checkForOtaUpdate() async throws -> Bool {
|
|
983
1033
|
let status = await getFreshGlassesStatus()
|
|
984
1034
|
guard status.connected else {
|
|
985
|
-
throw
|
|
1035
|
+
throw BluetoothSdkError(
|
|
986
1036
|
code: "glasses_not_connected",
|
|
987
1037
|
message: "Cannot check OTA update because glasses are not connected."
|
|
988
1038
|
)
|
|
989
1039
|
}
|
|
990
1040
|
guard !status.buildNumber.isEmpty else {
|
|
991
|
-
throw
|
|
1041
|
+
throw BluetoothSdkError(
|
|
992
1042
|
code: "missing_glasses_version",
|
|
993
1043
|
message: "Cannot check OTA update because glasses build number is unavailable."
|
|
994
1044
|
)
|
|
@@ -1017,7 +1067,7 @@ public final class MentraBluetoothSDK {
|
|
|
1017
1067
|
sendRequest: () -> Void
|
|
1018
1068
|
) async throws -> OtaQueryResult {
|
|
1019
1069
|
if pendingOtaQuery != nil {
|
|
1020
|
-
throw
|
|
1070
|
+
throw BluetoothSdkError(
|
|
1021
1071
|
code: "request_in_flight",
|
|
1022
1072
|
message: "An OTA status query is already waiting for a glasses response."
|
|
1023
1073
|
)
|
|
@@ -1048,7 +1098,7 @@ public final class MentraBluetoothSDK {
|
|
|
1048
1098
|
|
|
1049
1099
|
private func startOtaCommand(otaVersionUrl: String) async throws -> OtaStartAckEvent {
|
|
1050
1100
|
if pendingOtaStart != nil {
|
|
1051
|
-
throw
|
|
1101
|
+
throw BluetoothSdkError(
|
|
1052
1102
|
code: "request_in_flight",
|
|
1053
1103
|
message: "An OTA start command is already waiting for a glasses response."
|
|
1054
1104
|
)
|
|
@@ -1076,13 +1126,6 @@ public final class MentraBluetoothSDK {
|
|
|
1076
1126
|
|
|
1077
1127
|
func sendOtaQueryStatus() async throws -> OtaQueryResult { try await queryOtaStatus() }
|
|
1078
1128
|
|
|
1079
|
-
/// Re-run the glasses-side OTA version check after an internal clock-skew recovery.
|
|
1080
|
-
func retryOtaVersionCheck() async throws -> OtaQueryResult {
|
|
1081
|
-
try await performOtaQuery(operation: "OTA version retry") {
|
|
1082
|
-
DeviceManager.shared.retryOtaVersionCheck()
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
1129
|
private func getFreshGlassesStatus() async -> GlassesStatus {
|
|
1087
1130
|
let status = glassesStatus
|
|
1088
1131
|
if !status.connected || !status.buildNumber.isEmpty {
|
|
@@ -1118,7 +1161,7 @@ public final class MentraBluetoothSDK {
|
|
|
1118
1161
|
}
|
|
1119
1162
|
|
|
1120
1163
|
guard status.connected else {
|
|
1121
|
-
throw
|
|
1164
|
+
throw BluetoothSdkError(
|
|
1122
1165
|
code: "glasses_not_connected",
|
|
1123
1166
|
message: "Cannot check OTA update because glasses disconnected."
|
|
1124
1167
|
)
|
|
@@ -1152,7 +1195,7 @@ public final class MentraBluetoothSDK {
|
|
|
1152
1195
|
private func resolveOtaVersionUrl(status: GlassesStatus) throws -> String {
|
|
1153
1196
|
let deviceUrl = status.otaVersionUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1154
1197
|
if isLegacyAsgOtaStartBuild(status.buildNumber) {
|
|
1155
|
-
return
|
|
1198
|
+
return OtaManifestDefaults.legacyProdOtaVersionUrl
|
|
1156
1199
|
}
|
|
1157
1200
|
// SDK consumers are pinned to the manifest built for their SDK version.
|
|
1158
1201
|
// A future glasses-advertised URL should not silently change that pairing.
|
|
@@ -1164,7 +1207,7 @@ public final class MentraBluetoothSDK {
|
|
|
1164
1207
|
|
|
1165
1208
|
private func isLegacyAsgOtaStartBuild(_ buildNumber: String) -> Bool {
|
|
1166
1209
|
guard let parsed = Int(buildNumber) else { return false }
|
|
1167
|
-
return parsed <
|
|
1210
|
+
return parsed < 39
|
|
1168
1211
|
}
|
|
1169
1212
|
|
|
1170
1213
|
func sendShutdown() {
|
|
@@ -1274,7 +1317,7 @@ public final class MentraBluetoothSDK {
|
|
|
1274
1317
|
}
|
|
1275
1318
|
|
|
1276
1319
|
private func startStreamKeepAliveMonitor(streamId: String, intervalSeconds requestedIntervalSeconds: Int) {
|
|
1277
|
-
let intervalSeconds = requestedIntervalSeconds > 0 ? requestedIntervalSeconds :
|
|
1320
|
+
let intervalSeconds = requestedIntervalSeconds > 0 ? requestedIntervalSeconds : Self.defaultStreamKeepAliveIntervalSeconds
|
|
1278
1321
|
let tracker = ActiveStreamKeepAlive(streamId: streamId, intervalSeconds: intervalSeconds)
|
|
1279
1322
|
activeStreamKeepAlive = tracker
|
|
1280
1323
|
sendNextStreamKeepAlive(for: tracker)
|
|
@@ -1424,14 +1467,14 @@ public final class MentraBluetoothSDK {
|
|
|
1424
1467
|
)
|
|
1425
1468
|
}
|
|
1426
1469
|
|
|
1427
|
-
private func streamStatusError(_ event: StreamStatusEvent, code: String) ->
|
|
1470
|
+
private func streamStatusError(_ event: StreamStatusEvent, code: String) -> BluetoothSdkError {
|
|
1428
1471
|
let message: String
|
|
1429
1472
|
if case let .error(_, errorDetails, _, _) = event.status {
|
|
1430
1473
|
message = errorDetails
|
|
1431
1474
|
} else {
|
|
1432
1475
|
message = "Stream status \(event.state.rawValue)"
|
|
1433
1476
|
}
|
|
1434
|
-
return
|
|
1477
|
+
return BluetoothSdkError(code: code, message: message)
|
|
1435
1478
|
}
|
|
1436
1479
|
|
|
1437
1480
|
private func handlePhotoResponseForRequests(_ event: PhotoResponseEvent) {
|
|
@@ -1441,7 +1484,7 @@ public final class MentraBluetoothSDK {
|
|
|
1441
1484
|
pending.resolve(event)
|
|
1442
1485
|
case let .error(_, errorCode, errorMessage, _):
|
|
1443
1486
|
pending.reject(
|
|
1444
|
-
|
|
1487
|
+
BluetoothSdkError(
|
|
1445
1488
|
code: errorCode ?? "photo_request_failed",
|
|
1446
1489
|
message: errorMessage
|
|
1447
1490
|
)
|
|
@@ -1449,6 +1492,24 @@ public final class MentraBluetoothSDK {
|
|
|
1449
1492
|
}
|
|
1450
1493
|
}
|
|
1451
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
|
+
|
|
1452
1513
|
private func handleVideoRecordingStatusForRequests(_ event: VideoRecordingStatusEvent) {
|
|
1453
1514
|
guard let request = pendingVideoRecordingRequests[event.requestId] else { return }
|
|
1454
1515
|
if event.success {
|
|
@@ -1464,7 +1525,7 @@ public final class MentraBluetoothSDK {
|
|
|
1464
1525
|
}
|
|
1465
1526
|
} else {
|
|
1466
1527
|
request.pending.reject(
|
|
1467
|
-
|
|
1528
|
+
BluetoothSdkError(
|
|
1468
1529
|
code: event.status.isEmpty ? "video_recording_failed" : event.status,
|
|
1469
1530
|
message: event.details ?? "Video recording command failed."
|
|
1470
1531
|
)
|
|
@@ -1484,7 +1545,7 @@ public final class MentraBluetoothSDK {
|
|
|
1484
1545
|
}
|
|
1485
1546
|
} else {
|
|
1486
1547
|
request.pending.reject(
|
|
1487
|
-
|
|
1548
|
+
BluetoothSdkError(
|
|
1488
1549
|
code: "video_upload_failed",
|
|
1489
1550
|
message: event.errorMessage ?? "Video upload failed."
|
|
1490
1551
|
)
|
|
@@ -1498,7 +1559,7 @@ public final class MentraBluetoothSDK {
|
|
|
1498
1559
|
pending.resolve(event)
|
|
1499
1560
|
} else {
|
|
1500
1561
|
pending.reject(
|
|
1501
|
-
|
|
1562
|
+
BluetoothSdkError(
|
|
1502
1563
|
code: event.errorCode ?? "rgb_led_control_failed",
|
|
1503
1564
|
message: event.errorCode ?? "RGB LED command failed."
|
|
1504
1565
|
)
|
|
@@ -1511,7 +1572,7 @@ public final class MentraBluetoothSDK {
|
|
|
1511
1572
|
if isFailureStatus(event.status) {
|
|
1512
1573
|
let fallbackSetting = event.setting.isEmpty ? event.requestId : event.setting
|
|
1513
1574
|
pending.reject(
|
|
1514
|
-
|
|
1575
|
+
BluetoothSdkError(
|
|
1515
1576
|
code: event.errorCode ?? "\(event.setting.isEmpty ? "settings" : event.setting)_failed",
|
|
1516
1577
|
message: event.errorMessage ?? "Settings command \(fallbackSetting) failed."
|
|
1517
1578
|
)
|
|
@@ -1533,6 +1594,11 @@ public final class MentraBluetoothSDK {
|
|
|
1533
1594
|
request.pending.resolve(results)
|
|
1534
1595
|
}
|
|
1535
1596
|
|
|
1597
|
+
private func updateWifiScanLatestResults(_ results: [WifiScanResult]) {
|
|
1598
|
+
guard !results.isEmpty else { return }
|
|
1599
|
+
pendingWifiScan?.latestResults = results
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1536
1602
|
private func handleWifiStatusForRequests(_ event: WifiStatusEvent) {
|
|
1537
1603
|
guard let request = pendingWifiStatus else { return }
|
|
1538
1604
|
guard wifiStatusMatches(event.status, request: request) else { return }
|
|
@@ -1581,7 +1647,7 @@ public final class MentraBluetoothSDK {
|
|
|
1581
1647
|
pendingHotspotStatus = nil
|
|
1582
1648
|
}
|
|
1583
1649
|
request.pending.reject(
|
|
1584
|
-
|
|
1650
|
+
BluetoothSdkError(
|
|
1585
1651
|
code: "hotspot_command_failed",
|
|
1586
1652
|
message: event.message ?? "Hotspot command failed."
|
|
1587
1653
|
)
|
|
@@ -1721,6 +1787,7 @@ public final class MentraBluetoothSDK {
|
|
|
1721
1787
|
let networks = (data["networks"] as? [[String: Any]])?.map(WifiScanResult.init(values:)) ?? []
|
|
1722
1788
|
let hasCompletionFlag = data.keys.contains("scanComplete") || data.keys.contains("scan_complete")
|
|
1723
1789
|
let scanComplete = data["scanComplete"] as? Bool ?? data["scan_complete"] as? Bool ?? false
|
|
1790
|
+
updateWifiScanLatestResults(networks)
|
|
1724
1791
|
if scanComplete || !hasCompletionFlag {
|
|
1725
1792
|
handleWifiScanResultsForRequests(networks)
|
|
1726
1793
|
}
|
|
@@ -1739,6 +1806,10 @@ public final class MentraBluetoothSDK {
|
|
|
1739
1806
|
delegate?.mentraBluetoothSDK(self, didReceive: .photoResponse(event))
|
|
1740
1807
|
case "photo_status":
|
|
1741
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))
|
|
1742
1813
|
case "video_recording_status":
|
|
1743
1814
|
let event = VideoRecordingStatusEvent(values: data)
|
|
1744
1815
|
handleVideoRecordingStatusForRequests(event)
|
|
@@ -1761,11 +1832,6 @@ public final class MentraBluetoothSDK {
|
|
|
1761
1832
|
if !handleStreamKeepAliveAck(event) {
|
|
1762
1833
|
delegate?.mentraBluetoothSDK(self, didReceive: .keepAliveAck(event))
|
|
1763
1834
|
}
|
|
1764
|
-
case "ota_update_available":
|
|
1765
|
-
var resultValues = data
|
|
1766
|
-
resultValues["type"] = "ota_update_available"
|
|
1767
|
-
pendingOtaQuery?.resolve(OtaQueryResult(values: resultValues))
|
|
1768
|
-
delegate?.mentraBluetoothSDK(self, didReceive: .otaUpdateAvailable(OtaUpdateAvailableEvent(values: resultValues)))
|
|
1769
1835
|
case "ota_start_ack":
|
|
1770
1836
|
var values = data
|
|
1771
1837
|
values["type"] = "ota_start_ack"
|
|
@@ -1790,7 +1856,7 @@ public final class MentraBluetoothSDK {
|
|
|
1790
1856
|
case "pair_failure":
|
|
1791
1857
|
delegate?.mentraBluetoothSDK(
|
|
1792
1858
|
self,
|
|
1793
|
-
didFail:
|
|
1859
|
+
didFail: BluetoothSdkError(
|
|
1794
1860
|
code: "pair_failure",
|
|
1795
1861
|
message: data["error"] as? String ?? data.description
|
|
1796
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
|