@mentra/bluetooth-sdk 0.1.9 → 0.1.10
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 +116 -18
- package/android/build.gradle +289 -16
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/build.gradle +95 -6
- package/android/settings.gradle +3 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +42 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/build/BluetoothSdk.types.d.ts +241 -43
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +11 -10
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +36 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +30 -52
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +4 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +25 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +23 -0
- package/build/index.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +2 -2
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +3 -3
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +174 -70
- package/ios/LocalPhotoUploadServer.swift +370 -105
- package/ios/MentraBluetoothSDK.podspec +3 -1
- package/ios/MentraPhotoReceiverModule.swift +62 -10
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -0
- package/ios/Source/Bridge.swift +58 -19
- package/ios/Source/DeviceManager.swift +149 -17
- package/ios/Source/DeviceStore.swift +16 -6
- package/ios/Source/MentraBluetoothSDK.swift +803 -47
- package/ios/Source/ObservableStore.swift +6 -0
- package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
- package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
- package/ios/Source/controllers/ControllerManager.swift +4 -3
- package/ios/Source/controllers/R1.swift +3 -3
- package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
- package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
- package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
- package/ios/Source/services/PhoneMic.swift +1 -0
- package/ios/Source/sgcs/G1.swift +2 -2
- package/ios/Source/sgcs/G2.swift +645 -403
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +812 -319
- package/ios/Source/sgcs/MentraNex.swift +412 -182
- package/ios/Source/sgcs/SGCManager.swift +57 -3
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
- package/ios/Source/status/DeviceStatus.swift +974 -0
- package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
- package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
- package/ios/Source/stt/STTTools.swift +27 -12
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
- package/ios/Source/tts/TTSTools.swift +164 -0
- package/ios/Source/utils/JSCExperiment.swift +7 -7
- package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
- package/ios/Source/utils/MessageChunker.swift +78 -21
- package/ios/Source/utils/TarBz2Extractor.swift +53 -2
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +68 -4
- package/src/BluetoothSdk.types.ts +331 -59
- package/src/_private/BluetoothSdkModule.ts +113 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +58 -15
- package/src/react/useMentraBluetooth.ts +6 -4
- package/android/.project +0 -28
- package/android/lc3Lib/.project +0 -28
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Source/Status/DeviceStatus.swift +0 -471
- /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
- /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
- /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
- /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
- /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
|
@@ -8,6 +8,10 @@ private final class ActiveStreamKeepAlive {
|
|
|
8
8
|
var pendingAckId: String?
|
|
9
9
|
var missedAckCount = 0
|
|
10
10
|
var task: Task<Void, Never>?
|
|
11
|
+
// Missed-ACK counting only begins once the stream is confirmed live/coming up, so a slow
|
|
12
|
+
// startup (glasses can't ACK until they reach starting/streaming) can't trip a false
|
|
13
|
+
// keep-alive timeout before the stream is ever up.
|
|
14
|
+
var armed = false
|
|
11
15
|
|
|
12
16
|
init(streamId: String, intervalSeconds: Int) {
|
|
13
17
|
self.streamId = streamId
|
|
@@ -35,6 +39,108 @@ private final class ActiveScanSession {
|
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
@MainActor
|
|
43
|
+
private final class PendingWifiScan {
|
|
44
|
+
let pending: PendingResponse<[WifiScanResult]>
|
|
45
|
+
|
|
46
|
+
init(pending: PendingResponse<[WifiScanResult]>) {
|
|
47
|
+
self.pending = pending
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private enum WifiStatusOperation {
|
|
52
|
+
case connect
|
|
53
|
+
case forget
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@MainActor
|
|
57
|
+
private final class PendingWifiStatusRequest {
|
|
58
|
+
let operation: WifiStatusOperation
|
|
59
|
+
let ssid: String
|
|
60
|
+
let pending: PendingResponse<WifiStatusEvent>
|
|
61
|
+
|
|
62
|
+
init(operation: WifiStatusOperation, ssid: String, pending: PendingResponse<WifiStatusEvent>) {
|
|
63
|
+
self.operation = operation
|
|
64
|
+
self.ssid = ssid
|
|
65
|
+
self.pending = pending
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@MainActor
|
|
70
|
+
private final class PendingHotspotStatusRequest {
|
|
71
|
+
let enabled: Bool
|
|
72
|
+
let pending: PendingResponse<HotspotStatusEvent>
|
|
73
|
+
|
|
74
|
+
init(enabled: Bool, pending: PendingResponse<HotspotStatusEvent>) {
|
|
75
|
+
self.enabled = enabled
|
|
76
|
+
self.pending = pending
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@MainActor
|
|
81
|
+
private final class PendingVideoRecordingRequest {
|
|
82
|
+
let expectedStatus: String
|
|
83
|
+
let pending: PendingResponse<VideoRecordingStatusEvent>
|
|
84
|
+
|
|
85
|
+
init(expectedStatus: String, pending: PendingResponse<VideoRecordingStatusEvent>) {
|
|
86
|
+
self.expectedStatus = expectedStatus
|
|
87
|
+
self.pending = pending
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@MainActor
|
|
92
|
+
private final class PendingResponse<T> {
|
|
93
|
+
private let operation: String
|
|
94
|
+
private var continuation: CheckedContinuation<T, Error>?
|
|
95
|
+
private var timeoutTask: Task<Void, Never>?
|
|
96
|
+
private var result: Result<T, Error>?
|
|
97
|
+
private var completed = false
|
|
98
|
+
|
|
99
|
+
init(operation: String) {
|
|
100
|
+
self.operation = operation
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func resolve(_ value: T) {
|
|
104
|
+
guard !completed else { return }
|
|
105
|
+
completed = true
|
|
106
|
+
result = .success(value)
|
|
107
|
+
timeoutTask?.cancel()
|
|
108
|
+
continuation?.resume(returning: value)
|
|
109
|
+
continuation = nil
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func reject(_ error: Error) {
|
|
113
|
+
guard !completed else { return }
|
|
114
|
+
completed = true
|
|
115
|
+
result = .failure(error)
|
|
116
|
+
timeoutTask?.cancel()
|
|
117
|
+
continuation?.resume(throwing: error)
|
|
118
|
+
continuation = nil
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func wait(timeoutMs: Int = 15_000) async throws -> T {
|
|
122
|
+
if let result {
|
|
123
|
+
return try result.get()
|
|
124
|
+
}
|
|
125
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
126
|
+
self.continuation = continuation
|
|
127
|
+
timeoutTask = Task { @MainActor [weak self] in
|
|
128
|
+
do {
|
|
129
|
+
try await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000)
|
|
130
|
+
} catch {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
self?.reject(
|
|
134
|
+
BluetoothError(
|
|
135
|
+
code: "request_timeout",
|
|
136
|
+
message: "\(self?.operation ?? "Request") timed out waiting for glasses response."
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
38
144
|
@MainActor
|
|
39
145
|
public final class MentraBluetoothSDK {
|
|
40
146
|
public weak var delegate: MentraBluetoothSDKDelegate?
|
|
@@ -48,6 +154,19 @@ public final class MentraBluetoothSDK {
|
|
|
48
154
|
private var defaultDeviceApplyGeneration = 0
|
|
49
155
|
private var activeScanSessions: [UUID: ActiveScanSession] = [:]
|
|
50
156
|
private var activeStreamKeepAlive: ActiveStreamKeepAlive?
|
|
157
|
+
private var pendingPhotoRequests: [String: PendingResponse<PhotoResponseEvent>] = [:]
|
|
158
|
+
private var pendingVideoRecordingRequests: [String: PendingVideoRecordingRequest] = [:]
|
|
159
|
+
private var pendingRgbLedRequests: [String: PendingResponse<RgbLedControlResponseEvent>] = [:]
|
|
160
|
+
private var pendingSettingsRequests: [String: PendingResponse<SettingsAckEvent>] = [:]
|
|
161
|
+
private var pendingStreamStarts: [String: PendingResponse<StreamStatusEvent>] = [:]
|
|
162
|
+
private var pendingStreamStop: (streamId: String?, pending: PendingResponse<StreamStatusEvent>)?
|
|
163
|
+
private var pendingGalleryStatus: PendingResponse<GalleryStatusEvent>?
|
|
164
|
+
private var pendingOtaQuery: PendingResponse<OtaQueryResult>?
|
|
165
|
+
private var pendingOtaStart: PendingResponse<OtaStartAckEvent>?
|
|
166
|
+
private var pendingWifiScan: PendingWifiScan?
|
|
167
|
+
private var pendingWifiStatus: PendingWifiStatusRequest?
|
|
168
|
+
private var pendingHotspotStatus: PendingHotspotStatusRequest?
|
|
169
|
+
private var pendingVersionInfo: PendingResponse<VersionInfoResult>?
|
|
51
170
|
|
|
52
171
|
public init(configuration: MentraBluetoothSDKConfiguration = .default) {
|
|
53
172
|
self.configuration = configuration
|
|
@@ -247,6 +366,10 @@ public final class MentraBluetoothSDK {
|
|
|
247
366
|
DeviceManager.shared.showDashboard()
|
|
248
367
|
}
|
|
249
368
|
|
|
369
|
+
public func showNotificationsPanel() {
|
|
370
|
+
DeviceManager.shared.showNotificationsPanel()
|
|
371
|
+
}
|
|
372
|
+
|
|
250
373
|
func setBrightness(_ level: Int, autoMode: Bool? = nil) async throws {
|
|
251
374
|
if let autoMode {
|
|
252
375
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "auto_brightness", autoMode)
|
|
@@ -275,6 +398,14 @@ public final class MentraBluetoothSDK {
|
|
|
275
398
|
)
|
|
276
399
|
}
|
|
277
400
|
|
|
401
|
+
func setCalendarEvents(_ events: [CalendarEvent]) async throws {
|
|
402
|
+
DeviceStore.shared.apply(
|
|
403
|
+
ObservableStore.bluetoothCategory,
|
|
404
|
+
"calendar_events",
|
|
405
|
+
events.map(\.dictionary)
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
278
409
|
public func setHeadUpAngle(_ angleDegrees: Int) async throws {
|
|
279
410
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "head_up_angle", angleDegrees)
|
|
280
411
|
}
|
|
@@ -283,42 +414,112 @@ public final class MentraBluetoothSDK {
|
|
|
283
414
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "screen_disabled", disabled)
|
|
284
415
|
}
|
|
285
416
|
|
|
286
|
-
public func setGalleryModeEnabled(_ enabled: Bool) async throws {
|
|
287
|
-
|
|
417
|
+
public func setGalleryModeEnabled(_ enabled: Bool) async throws -> SettingsAckEvent {
|
|
418
|
+
try await performSettingsCommand(
|
|
419
|
+
setting: "gallery_mode",
|
|
420
|
+
updateStore: { _ in DeviceStore.shared.set(ObservableStore.bluetoothCategory, "gallery_mode", enabled) },
|
|
421
|
+
send: { requestId in try DeviceManager.shared.sendGalleryMode(requestId: requestId, enabled: enabled) }
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private func performSettingsCommand(
|
|
426
|
+
setting: String,
|
|
427
|
+
updateStore: (SettingsAckEvent) -> Void,
|
|
428
|
+
send: (String) throws -> Void
|
|
429
|
+
) async throws -> SettingsAckEvent {
|
|
430
|
+
let requestId = "settings-\(setting)-\(UUID().uuidString)"
|
|
431
|
+
let pending = PendingResponse<SettingsAckEvent>(operation: "set \(setting)")
|
|
432
|
+
pendingSettingsRequests[requestId] = pending
|
|
433
|
+
do {
|
|
434
|
+
try send(requestId)
|
|
435
|
+
let ack = try await pending.wait()
|
|
436
|
+
updateStore(ack)
|
|
437
|
+
pendingSettingsRequests.removeValue(forKey: requestId)
|
|
438
|
+
return ack
|
|
439
|
+
} catch {
|
|
440
|
+
pendingSettingsRequests.removeValue(forKey: requestId)
|
|
441
|
+
throw error
|
|
442
|
+
}
|
|
288
443
|
}
|
|
289
444
|
|
|
290
445
|
public func setVoiceActivityDetectionEnabled(_ enabled: Bool) async throws {
|
|
291
446
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "voice_activity_detection_enabled", enabled)
|
|
292
447
|
}
|
|
293
448
|
|
|
294
|
-
public func setButtonPhotoSettings(size: ButtonPhotoSize) async throws {
|
|
295
|
-
|
|
449
|
+
public func setButtonPhotoSettings(size: ButtonPhotoSize) async throws -> SettingsAckEvent {
|
|
450
|
+
try await performSettingsCommand(
|
|
451
|
+
setting: "button_photo",
|
|
452
|
+
updateStore: { _ in DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_size", size.rawValue) },
|
|
453
|
+
send: { requestId in try DeviceManager.shared.sendButtonPhotoSettings(requestId: requestId, size: size.rawValue) }
|
|
454
|
+
)
|
|
296
455
|
}
|
|
297
456
|
|
|
298
|
-
public func setButtonPhotoSettings(_ settings: ButtonPhotoSettings) async throws {
|
|
457
|
+
public func setButtonPhotoSettings(_ settings: ButtonPhotoSettings) async throws -> SettingsAckEvent {
|
|
299
458
|
try await setButtonPhotoSettings(size: settings.size)
|
|
300
459
|
}
|
|
301
460
|
|
|
302
|
-
public func setButtonVideoRecordingSettings(width: Int, height: Int, fps: Int) async throws {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
461
|
+
public func setButtonVideoRecordingSettings(width: Int, height: Int, fps: Int) async throws -> SettingsAckEvent {
|
|
462
|
+
try await performSettingsCommand(
|
|
463
|
+
setting: "button_video_recording",
|
|
464
|
+
updateStore: { _ in
|
|
465
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_width", width)
|
|
466
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_height", height)
|
|
467
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_fps", fps)
|
|
468
|
+
},
|
|
469
|
+
send: { requestId in
|
|
470
|
+
try DeviceManager.shared.sendButtonVideoRecordingSettings(
|
|
471
|
+
requestId: requestId,
|
|
472
|
+
width: width,
|
|
473
|
+
height: height,
|
|
474
|
+
fps: fps
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
)
|
|
306
478
|
}
|
|
307
479
|
|
|
308
|
-
public func setButtonVideoRecordingSettings(_ settings: ButtonVideoRecordingSettings) async throws {
|
|
480
|
+
public func setButtonVideoRecordingSettings(_ settings: ButtonVideoRecordingSettings) async throws -> SettingsAckEvent {
|
|
309
481
|
try await setButtonVideoRecordingSettings(width: settings.width, height: settings.height, fps: settings.fps)
|
|
310
482
|
}
|
|
311
483
|
|
|
312
|
-
public func setButtonCameraLed(enabled: Bool) async throws {
|
|
313
|
-
|
|
484
|
+
public func setButtonCameraLed(enabled: Bool) async throws -> SettingsAckEvent {
|
|
485
|
+
try await performSettingsCommand(
|
|
486
|
+
setting: "button_camera_led",
|
|
487
|
+
updateStore: { _ in DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_camera_led", enabled) },
|
|
488
|
+
send: { requestId in try DeviceManager.shared.sendButtonCameraLedSetting(requestId: requestId, enabled: enabled) }
|
|
489
|
+
)
|
|
314
490
|
}
|
|
315
491
|
|
|
316
|
-
public func setButtonMaxRecordingTime(minutes: Int) async throws {
|
|
317
|
-
|
|
492
|
+
public func setButtonMaxRecordingTime(minutes: Int) async throws -> SettingsAckEvent {
|
|
493
|
+
try await performSettingsCommand(
|
|
494
|
+
setting: "button_max_recording_time",
|
|
495
|
+
updateStore: { _ in
|
|
496
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_max_recording_time", minutes)
|
|
497
|
+
},
|
|
498
|
+
send: { requestId in
|
|
499
|
+
try DeviceManager.shared.sendButtonMaxRecordingTime(requestId: requestId, minutes: minutes)
|
|
500
|
+
}
|
|
501
|
+
)
|
|
318
502
|
}
|
|
319
503
|
|
|
320
|
-
public func setCameraFov(_ fov: CameraFov) async throws {
|
|
321
|
-
|
|
504
|
+
public func setCameraFov(_ fov: CameraFov) async throws -> CameraFovResult {
|
|
505
|
+
let ack = try await performSettingsCommand(
|
|
506
|
+
setting: "camera_fov",
|
|
507
|
+
updateStore: { _ in },
|
|
508
|
+
send: { requestId in
|
|
509
|
+
try DeviceManager.shared.sendCameraFovSetting(
|
|
510
|
+
requestId: requestId,
|
|
511
|
+
fov: fov.fov,
|
|
512
|
+
roiPosition: fov.roiPosition.rawValue
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
)
|
|
516
|
+
let result = try CameraFovResult.from(ack: ack, fallback: fov)
|
|
517
|
+
DeviceStore.shared.set(
|
|
518
|
+
ObservableStore.bluetoothCategory,
|
|
519
|
+
"camera_fov",
|
|
520
|
+
["fov": result.fov, "roi_position": result.roiPosition.rawValue]
|
|
521
|
+
)
|
|
522
|
+
return result
|
|
322
523
|
}
|
|
323
524
|
|
|
324
525
|
public func setMicState(
|
|
@@ -361,39 +562,127 @@ public final class MentraBluetoothSDK {
|
|
|
361
562
|
}
|
|
362
563
|
|
|
363
564
|
public func getGlassesMediaVolume() async throws -> GlassesMediaVolumeGetResult {
|
|
364
|
-
GlassesMediaVolumeGetResult(values:
|
|
565
|
+
try GlassesMediaVolumeGetResult(values: await DeviceManager.shared.getGlassesMediaVolume())
|
|
365
566
|
}
|
|
366
567
|
|
|
367
568
|
public func setGlassesMediaVolume(_ level: Int) async throws -> GlassesMediaVolumeSetResult {
|
|
368
|
-
guard (0...15).contains(level) else {
|
|
569
|
+
guard (0 ... 15).contains(level) else {
|
|
369
570
|
throw BluetoothError(
|
|
370
571
|
code: "invalid_volume_level",
|
|
371
572
|
message: "Glasses media volume must be between 0 and 15."
|
|
372
573
|
)
|
|
373
574
|
}
|
|
374
|
-
return GlassesMediaVolumeSetResult(values:
|
|
575
|
+
return try GlassesMediaVolumeSetResult(values: await DeviceManager.shared.setGlassesMediaVolume(level: level))
|
|
375
576
|
}
|
|
376
577
|
|
|
377
|
-
public func requestWifiScan() {
|
|
578
|
+
public func requestWifiScan() async throws -> [WifiScanResult] {
|
|
579
|
+
guard pendingWifiScan == nil else {
|
|
580
|
+
throw BluetoothError(
|
|
581
|
+
code: "request_in_flight",
|
|
582
|
+
message: "A WiFi scan is already waiting for a glasses response."
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
let pending = PendingResponse<[WifiScanResult]>(operation: "WiFi scan request")
|
|
586
|
+
pendingWifiScan = PendingWifiScan(pending: pending)
|
|
378
587
|
DeviceManager.shared.requestWifiScan()
|
|
588
|
+
do {
|
|
589
|
+
let results = try await pending.wait()
|
|
590
|
+
if pendingWifiScan?.pending === pending {
|
|
591
|
+
pendingWifiScan = nil
|
|
592
|
+
}
|
|
593
|
+
return results
|
|
594
|
+
} catch {
|
|
595
|
+
if pendingWifiScan?.pending === pending {
|
|
596
|
+
pendingWifiScan = nil
|
|
597
|
+
}
|
|
598
|
+
throw error
|
|
599
|
+
}
|
|
379
600
|
}
|
|
380
601
|
|
|
381
|
-
public func sendWifiCredentials(ssid: String, password: String) {
|
|
602
|
+
public func sendWifiCredentials(ssid: String, password: String) async throws -> WifiStatusEvent {
|
|
603
|
+
guard pendingWifiStatus == nil else {
|
|
604
|
+
throw BluetoothError(
|
|
605
|
+
code: "request_in_flight",
|
|
606
|
+
message: "A WiFi status command is already waiting for a glasses response."
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi connect request")
|
|
610
|
+
pendingWifiStatus = PendingWifiStatusRequest(operation: .connect, ssid: ssid, pending: pending)
|
|
382
611
|
DeviceManager.shared.sendWifiCredentials(ssid, password)
|
|
612
|
+
do {
|
|
613
|
+
let event = try await pending.wait()
|
|
614
|
+
if pendingWifiStatus?.pending === pending {
|
|
615
|
+
pendingWifiStatus = nil
|
|
616
|
+
}
|
|
617
|
+
return event
|
|
618
|
+
} catch {
|
|
619
|
+
if pendingWifiStatus?.pending === pending {
|
|
620
|
+
pendingWifiStatus = nil
|
|
621
|
+
}
|
|
622
|
+
throw error
|
|
623
|
+
}
|
|
383
624
|
}
|
|
384
625
|
|
|
385
|
-
public func forgetWifiNetwork(ssid: String) {
|
|
626
|
+
public func forgetWifiNetwork(ssid: String) async throws -> WifiStatusEvent {
|
|
627
|
+
guard pendingWifiStatus == nil else {
|
|
628
|
+
throw BluetoothError(
|
|
629
|
+
code: "request_in_flight",
|
|
630
|
+
message: "A WiFi status command is already waiting for a glasses response."
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi forget request")
|
|
634
|
+
pendingWifiStatus = PendingWifiStatusRequest(operation: .forget, ssid: ssid, pending: pending)
|
|
386
635
|
DeviceManager.shared.forgetWifiNetwork(ssid)
|
|
636
|
+
do {
|
|
637
|
+
let event = try await pending.wait()
|
|
638
|
+
if pendingWifiStatus?.pending === pending {
|
|
639
|
+
pendingWifiStatus = nil
|
|
640
|
+
}
|
|
641
|
+
return event
|
|
642
|
+
} catch {
|
|
643
|
+
if pendingWifiStatus?.pending === pending {
|
|
644
|
+
pendingWifiStatus = nil
|
|
645
|
+
}
|
|
646
|
+
throw error
|
|
647
|
+
}
|
|
387
648
|
}
|
|
388
649
|
|
|
389
|
-
public func setHotspotState(enabled: Bool) {
|
|
650
|
+
public func setHotspotState(enabled: Bool) async throws -> HotspotStatusEvent {
|
|
651
|
+
guard pendingHotspotStatus == nil else {
|
|
652
|
+
throw BluetoothError(
|
|
653
|
+
code: "request_in_flight",
|
|
654
|
+
message: "A hotspot command is already waiting for a glasses response."
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
let pending = PendingResponse<HotspotStatusEvent>(
|
|
658
|
+
operation: "hotspot \(enabled ? "enable" : "disable") request"
|
|
659
|
+
)
|
|
660
|
+
pendingHotspotStatus = PendingHotspotStatusRequest(enabled: enabled, pending: pending)
|
|
390
661
|
DeviceManager.shared.setHotspotState(enabled)
|
|
662
|
+
do {
|
|
663
|
+
let event = try await pending.wait()
|
|
664
|
+
if pendingHotspotStatus?.pending === pending {
|
|
665
|
+
pendingHotspotStatus = nil
|
|
666
|
+
}
|
|
667
|
+
return event
|
|
668
|
+
} catch {
|
|
669
|
+
if pendingHotspotStatus?.pending === pending {
|
|
670
|
+
pendingHotspotStatus = nil
|
|
671
|
+
}
|
|
672
|
+
throw error
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
func setSystemTime(timestampMs: Int64) {
|
|
677
|
+
DeviceManager.shared.setSystemTime(timestampMs)
|
|
391
678
|
}
|
|
392
679
|
|
|
393
|
-
public func requestPhoto(_ request: PhotoRequest) {
|
|
680
|
+
public func requestPhoto(_ request: PhotoRequest) async throws -> PhotoResponseEvent {
|
|
394
681
|
Bridge.log(
|
|
395
682
|
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=\(request.requestId) appId=\(request.appId)"
|
|
396
683
|
)
|
|
684
|
+
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(request.requestId)")
|
|
685
|
+
pendingPhotoRequests[request.requestId] = pending
|
|
397
686
|
DeviceManager.shared.requestPhoto(
|
|
398
687
|
request.requestId,
|
|
399
688
|
request.appId,
|
|
@@ -402,32 +691,73 @@ public final class MentraBluetoothSDK {
|
|
|
402
691
|
request.authToken,
|
|
403
692
|
request.compress?.rawValue,
|
|
404
693
|
request.flash,
|
|
694
|
+
request.save,
|
|
405
695
|
request.sound,
|
|
406
696
|
exposureTimeNs: request.exposureTimeNs,
|
|
407
697
|
iso: request.iso
|
|
408
698
|
)
|
|
699
|
+
do {
|
|
700
|
+
let event = try await pending.wait()
|
|
701
|
+
pendingPhotoRequests.removeValue(forKey: request.requestId)
|
|
702
|
+
return event
|
|
703
|
+
} catch {
|
|
704
|
+
pendingPhotoRequests.removeValue(forKey: request.requestId)
|
|
705
|
+
throw error
|
|
706
|
+
}
|
|
409
707
|
}
|
|
410
708
|
|
|
411
|
-
public func queryGalleryStatus() {
|
|
709
|
+
public func queryGalleryStatus() async throws -> GalleryStatusEvent {
|
|
710
|
+
if pendingGalleryStatus != nil {
|
|
711
|
+
throw BluetoothError(
|
|
712
|
+
code: "request_in_flight",
|
|
713
|
+
message: "A gallery status query is already waiting for a glasses response."
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
let pending = PendingResponse<GalleryStatusEvent>(operation: "gallery status query")
|
|
717
|
+
pendingGalleryStatus = pending
|
|
412
718
|
DeviceManager.shared.queryGalleryStatus()
|
|
719
|
+
do {
|
|
720
|
+
let event = try await pending.wait()
|
|
721
|
+
if pendingGalleryStatus === pending {
|
|
722
|
+
pendingGalleryStatus = nil
|
|
723
|
+
}
|
|
724
|
+
return event
|
|
725
|
+
} catch {
|
|
726
|
+
if pendingGalleryStatus === pending {
|
|
727
|
+
pendingGalleryStatus = nil
|
|
728
|
+
}
|
|
729
|
+
throw error
|
|
730
|
+
}
|
|
413
731
|
}
|
|
414
732
|
|
|
415
|
-
public func startStream(_ request: StreamRequest) {
|
|
733
|
+
public func startStream(_ request: StreamRequest) async throws -> StreamStatusEvent {
|
|
416
734
|
var values = request.values
|
|
417
735
|
let streamId = stringValue(values, "streamId").flatMap { $0.isEmpty ? nil : $0 } ?? "sdk-\(UUID().uuidString)"
|
|
418
736
|
values["streamId"] = streamId
|
|
737
|
+
let pending = PendingResponse<StreamStatusEvent>(operation: "start stream \(streamId)")
|
|
738
|
+
pendingStreamStarts[streamId] = pending
|
|
419
739
|
stopStreamKeepAliveMonitor()
|
|
420
740
|
DeviceManager.shared.startStream(values)
|
|
421
|
-
|
|
422
|
-
|
|
741
|
+
do {
|
|
742
|
+
let event = try await pending.wait(timeoutMs: 30_000)
|
|
743
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
744
|
+
if request.keepAlive, !request.isExternallyManagedKeepAlive {
|
|
745
|
+
startStreamKeepAliveMonitor(streamId: streamId, intervalSeconds: request.keepAliveIntervalSeconds)
|
|
746
|
+
}
|
|
747
|
+
return event
|
|
748
|
+
} catch {
|
|
749
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
750
|
+
throw error
|
|
423
751
|
}
|
|
424
752
|
}
|
|
425
753
|
|
|
426
|
-
func
|
|
754
|
+
func sendExternallyManagedStreamKeepAlive(_ request: StreamKeepAliveRequest) {
|
|
427
755
|
DeviceManager.shared.keepStreamAlive(request.values)
|
|
428
756
|
}
|
|
429
757
|
|
|
430
|
-
public func rgbLedControl(_ request: RgbLedRequest) {
|
|
758
|
+
public func rgbLedControl(_ request: RgbLedRequest) async throws -> RgbLedControlResponseEvent {
|
|
759
|
+
let pending = PendingResponse<RgbLedControlResponseEvent>(operation: "RGB LED command \(request.requestId)")
|
|
760
|
+
pendingRgbLedRequests[request.requestId] = pending
|
|
431
761
|
DeviceManager.shared.rgbLedControl(
|
|
432
762
|
requestId: request.requestId,
|
|
433
763
|
packageName: request.packageName,
|
|
@@ -437,37 +767,196 @@ public final class MentraBluetoothSDK {
|
|
|
437
767
|
offDurationMs: request.offDurationMs,
|
|
438
768
|
count: request.count
|
|
439
769
|
)
|
|
770
|
+
do {
|
|
771
|
+
let event = try await pending.wait()
|
|
772
|
+
pendingRgbLedRequests.removeValue(forKey: request.requestId)
|
|
773
|
+
return event
|
|
774
|
+
} catch {
|
|
775
|
+
pendingRgbLedRequests.removeValue(forKey: request.requestId)
|
|
776
|
+
throw error
|
|
777
|
+
}
|
|
440
778
|
}
|
|
441
779
|
|
|
442
|
-
public func stopStream() {
|
|
780
|
+
public func stopStream() async throws -> StreamStatusEvent {
|
|
781
|
+
guard pendingStreamStop == nil else {
|
|
782
|
+
throw BluetoothError(
|
|
783
|
+
code: "request_in_flight",
|
|
784
|
+
message: "A stream stop command is already waiting for a glasses response."
|
|
785
|
+
)
|
|
786
|
+
}
|
|
787
|
+
let pending = PendingResponse<StreamStatusEvent>(operation: "stop stream")
|
|
788
|
+
pendingStreamStop = (streamId: activeStreamKeepAlive?.streamId, pending: pending)
|
|
443
789
|
stopStreamKeepAliveMonitor()
|
|
444
790
|
DeviceManager.shared.stopStream()
|
|
791
|
+
do {
|
|
792
|
+
let event = try await pending.wait(timeoutMs: 15_000)
|
|
793
|
+
if pendingStreamStop?.pending === pending {
|
|
794
|
+
pendingStreamStop = nil
|
|
795
|
+
}
|
|
796
|
+
return event
|
|
797
|
+
} catch {
|
|
798
|
+
if pendingStreamStop?.pending === pending {
|
|
799
|
+
pendingStreamStop = nil
|
|
800
|
+
}
|
|
801
|
+
throw error
|
|
802
|
+
}
|
|
445
803
|
}
|
|
446
804
|
|
|
447
|
-
public func startVideoRecording(_ request: VideoRecordingRequest) {
|
|
805
|
+
public func startVideoRecording(_ request: VideoRecordingRequest) async throws -> VideoRecordingStatusEvent {
|
|
806
|
+
guard !request.requestId.isEmpty else {
|
|
807
|
+
throw BluetoothError(code: "missing_request_id", message: "requestId is required to start video recording.")
|
|
808
|
+
}
|
|
809
|
+
let pending = PendingResponse<VideoRecordingStatusEvent>(
|
|
810
|
+
operation: "start video recording \(request.requestId)"
|
|
811
|
+
)
|
|
812
|
+
guard pendingVideoRecordingRequests[request.requestId] == nil else {
|
|
813
|
+
throw BluetoothError(
|
|
814
|
+
code: "request_in_flight",
|
|
815
|
+
message: "A video recording command is already waiting for requestId \(request.requestId)."
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
pendingVideoRecordingRequests[request.requestId] = PendingVideoRecordingRequest(
|
|
819
|
+
expectedStatus: "recording_started",
|
|
820
|
+
pending: pending
|
|
821
|
+
)
|
|
448
822
|
DeviceManager.shared.startVideoRecording(
|
|
449
823
|
request.requestId,
|
|
450
824
|
request.save,
|
|
451
|
-
request.sound
|
|
825
|
+
request.sound,
|
|
826
|
+
request.width,
|
|
827
|
+
request.height,
|
|
828
|
+
request.fps
|
|
452
829
|
)
|
|
830
|
+
do {
|
|
831
|
+
let event = try await pending.wait()
|
|
832
|
+
pendingVideoRecordingRequests.removeValue(forKey: request.requestId)
|
|
833
|
+
return event
|
|
834
|
+
} catch {
|
|
835
|
+
pendingVideoRecordingRequests.removeValue(forKey: request.requestId)
|
|
836
|
+
throw error
|
|
837
|
+
}
|
|
453
838
|
}
|
|
454
839
|
|
|
455
|
-
public func stopVideoRecording(requestId: String) {
|
|
840
|
+
public func stopVideoRecording(requestId: String) async throws -> VideoRecordingStatusEvent {
|
|
841
|
+
guard !requestId.isEmpty else {
|
|
842
|
+
throw BluetoothError(code: "missing_request_id", message: "requestId is required to stop video recording.")
|
|
843
|
+
}
|
|
844
|
+
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "stop video recording \(requestId)")
|
|
845
|
+
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
846
|
+
throw BluetoothError(
|
|
847
|
+
code: "request_in_flight",
|
|
848
|
+
message: "A video recording command is already waiting for requestId \(requestId)."
|
|
849
|
+
)
|
|
850
|
+
}
|
|
851
|
+
pendingVideoRecordingRequests[requestId] = PendingVideoRecordingRequest(
|
|
852
|
+
expectedStatus: "recording_stopped",
|
|
853
|
+
pending: pending
|
|
854
|
+
)
|
|
456
855
|
DeviceManager.shared.stopVideoRecording(requestId)
|
|
856
|
+
do {
|
|
857
|
+
let event = try await pending.wait()
|
|
858
|
+
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
859
|
+
return event
|
|
860
|
+
} catch {
|
|
861
|
+
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
862
|
+
throw error
|
|
863
|
+
}
|
|
457
864
|
}
|
|
458
865
|
|
|
459
|
-
public func requestVersionInfo() {
|
|
866
|
+
public func requestVersionInfo() async throws -> VersionInfoResult {
|
|
867
|
+
guard pendingVersionInfo == nil else {
|
|
868
|
+
throw BluetoothError(
|
|
869
|
+
code: "request_in_flight",
|
|
870
|
+
message: "A version info request is already waiting for a glasses response."
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
let pending = PendingResponse<VersionInfoResult>(operation: "version info request")
|
|
874
|
+
pendingVersionInfo = pending
|
|
460
875
|
DeviceManager.shared.requestVersionInfo()
|
|
876
|
+
do {
|
|
877
|
+
let status = try await pending.wait()
|
|
878
|
+
if pendingVersionInfo === pending {
|
|
879
|
+
pendingVersionInfo = nil
|
|
880
|
+
}
|
|
881
|
+
return status
|
|
882
|
+
} catch {
|
|
883
|
+
if pendingVersionInfo === pending {
|
|
884
|
+
pendingVersionInfo = nil
|
|
885
|
+
}
|
|
886
|
+
throw error
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/// Ask connected Mentra Live glasses to check/report OTA availability and status.
|
|
891
|
+
public func checkForOtaUpdate() async throws -> OtaQueryResult {
|
|
892
|
+
try await performOtaQuery(operation: "OTA status query") {
|
|
893
|
+
DeviceManager.shared.sendOtaQueryStatus()
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private func performOtaQuery(
|
|
898
|
+
operation: String,
|
|
899
|
+
sendRequest: () -> Void
|
|
900
|
+
) async throws -> OtaQueryResult {
|
|
901
|
+
if pendingOtaQuery != nil {
|
|
902
|
+
throw BluetoothError(
|
|
903
|
+
code: "request_in_flight",
|
|
904
|
+
message: "An OTA status query is already waiting for a glasses response."
|
|
905
|
+
)
|
|
906
|
+
}
|
|
907
|
+
let pending = PendingResponse<OtaQueryResult>(operation: operation)
|
|
908
|
+
pendingOtaQuery = pending
|
|
909
|
+
sendRequest()
|
|
910
|
+
do {
|
|
911
|
+
let result = try await pending.wait()
|
|
912
|
+
if pendingOtaQuery === pending {
|
|
913
|
+
pendingOtaQuery = nil
|
|
914
|
+
}
|
|
915
|
+
return result
|
|
916
|
+
} catch {
|
|
917
|
+
if pendingOtaQuery === pending {
|
|
918
|
+
pendingOtaQuery = nil
|
|
919
|
+
}
|
|
920
|
+
throw error
|
|
921
|
+
}
|
|
461
922
|
}
|
|
462
923
|
|
|
463
|
-
|
|
924
|
+
/// Start the OTA flow after your app has presented the available update to the user.
|
|
925
|
+
public func startOtaUpdate() async throws -> OtaStartAckEvent {
|
|
926
|
+
if pendingOtaStart != nil {
|
|
927
|
+
throw BluetoothError(
|
|
928
|
+
code: "request_in_flight",
|
|
929
|
+
message: "An OTA start command is already waiting for a glasses response."
|
|
930
|
+
)
|
|
931
|
+
}
|
|
932
|
+
let pending = PendingResponse<OtaStartAckEvent>(operation: "OTA start command")
|
|
933
|
+
pendingOtaStart = pending
|
|
464
934
|
DeviceManager.shared.sendOtaStart()
|
|
935
|
+
do {
|
|
936
|
+
let event = try await pending.wait()
|
|
937
|
+
if pendingOtaStart === pending {
|
|
938
|
+
pendingOtaStart = nil
|
|
939
|
+
}
|
|
940
|
+
return event
|
|
941
|
+
} catch {
|
|
942
|
+
if pendingOtaStart === pending {
|
|
943
|
+
pendingOtaStart = nil
|
|
944
|
+
}
|
|
945
|
+
throw error
|
|
946
|
+
}
|
|
465
947
|
}
|
|
466
948
|
|
|
467
|
-
|
|
468
|
-
|
|
949
|
+
/// Re-run the glasses-side OTA version check, mainly after correcting clock skew/TLS failures.
|
|
950
|
+
public func retryOtaVersionCheck() async throws -> OtaQueryResult {
|
|
951
|
+
try await performOtaQuery(operation: "OTA version retry") {
|
|
952
|
+
DeviceManager.shared.retryOtaVersionCheck()
|
|
953
|
+
}
|
|
469
954
|
}
|
|
470
955
|
|
|
956
|
+
func sendOtaStart() async throws -> OtaStartAckEvent { try await startOtaUpdate() }
|
|
957
|
+
|
|
958
|
+
func sendOtaQueryStatus() async throws -> OtaQueryResult { try await checkForOtaUpdate() }
|
|
959
|
+
|
|
471
960
|
func sendShutdown() {
|
|
472
961
|
DeviceManager.shared.sendShutdown()
|
|
473
962
|
}
|
|
@@ -508,7 +997,7 @@ public final class MentraBluetoothSDK {
|
|
|
508
997
|
private func sendNextStreamKeepAlive(for tracker: ActiveStreamKeepAlive) {
|
|
509
998
|
guard activeStreamKeepAlive === tracker else { return }
|
|
510
999
|
|
|
511
|
-
if tracker.pendingAckId != nil {
|
|
1000
|
+
if tracker.armed, tracker.pendingAckId != nil {
|
|
512
1001
|
tracker.missedAckCount += 1
|
|
513
1002
|
if tracker.missedAckCount >= 3 {
|
|
514
1003
|
activeStreamKeepAlive = nil
|
|
@@ -522,6 +1011,8 @@ public final class MentraBluetoothSDK {
|
|
|
522
1011
|
)
|
|
523
1012
|
)
|
|
524
1013
|
delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(event))
|
|
1014
|
+
stopStreamKeepAliveMonitor()
|
|
1015
|
+
DeviceManager.shared.stopStream()
|
|
525
1016
|
return
|
|
526
1017
|
}
|
|
527
1018
|
}
|
|
@@ -553,9 +1044,9 @@ public final class MentraBluetoothSDK {
|
|
|
553
1044
|
}
|
|
554
1045
|
|
|
555
1046
|
private func handleStreamStatusForKeepAlive(_ status: StreamStatus) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
{
|
|
1047
|
+
guard let streamId = status.streamId,
|
|
1048
|
+
activeStreamKeepAlive?.streamId == streamId
|
|
1049
|
+
else {
|
|
559
1050
|
return
|
|
560
1051
|
}
|
|
561
1052
|
|
|
@@ -563,10 +1054,222 @@ public final class MentraBluetoothSDK {
|
|
|
563
1054
|
case .stopped, .stopping, .error, .reconnectFailed:
|
|
564
1055
|
stopStreamKeepAliveMonitor()
|
|
565
1056
|
default:
|
|
566
|
-
|
|
1057
|
+
// A non-terminal status means the stream is live or coming up and the glasses can
|
|
1058
|
+
// now ACK; arm the missed-ACK detector from here so a slow startup before the first
|
|
1059
|
+
// ACK can't trip a false keep-alive timeout. On the arming transition, drop any
|
|
1060
|
+
// pre-arm bookkeeping so a stale unacked id can't immediately count as a miss.
|
|
1061
|
+
if let tracker = activeStreamKeepAlive, !tracker.armed {
|
|
1062
|
+
tracker.armed = true
|
|
1063
|
+
tracker.pendingAckId = nil
|
|
1064
|
+
tracker.missedAckCount = 0
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
private func handleStreamStatusForRequests(_ event: StreamStatusEvent) {
|
|
1070
|
+
if let (streamId, pending) = matchingStreamStart(for: event) {
|
|
1071
|
+
switch event.state {
|
|
1072
|
+
case .streaming:
|
|
1073
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1074
|
+
pending.resolve(event)
|
|
1075
|
+
case .error, .reconnectFailed, .stopped:
|
|
1076
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1077
|
+
pending.reject(streamStatusError(event, code: "stream_start_failed"))
|
|
1078
|
+
default:
|
|
1079
|
+
break
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if let stop = pendingStreamStop, streamStatus(event, matches: stop.streamId) {
|
|
1084
|
+
if isAlreadyStoppedStreamStatus(event) {
|
|
1085
|
+
if pendingStreamStop?.pending === stop.pending {
|
|
1086
|
+
pendingStreamStop = nil
|
|
1087
|
+
}
|
|
1088
|
+
stop.pending.resolve(stoppedStreamEvent(from: event, fallbackStreamId: stop.streamId))
|
|
1089
|
+
} else if event.state == .error || event.state == .reconnectFailed {
|
|
1090
|
+
if pendingStreamStop?.pending === stop.pending {
|
|
1091
|
+
pendingStreamStop = nil
|
|
1092
|
+
}
|
|
1093
|
+
stop.pending.reject(streamStatusError(event, code: "stream_stop_failed"))
|
|
1094
|
+
}
|
|
567
1095
|
}
|
|
568
1096
|
}
|
|
569
1097
|
|
|
1098
|
+
private func matchingStreamStart(for event: StreamStatusEvent) -> (String, PendingResponse<StreamStatusEvent>)? {
|
|
1099
|
+
if let streamId = event.streamId, !streamId.isEmpty {
|
|
1100
|
+
guard let pending = pendingStreamStarts[streamId] else { return nil }
|
|
1101
|
+
return (streamId, pending)
|
|
1102
|
+
}
|
|
1103
|
+
if pendingStreamStarts.count == 1, let entry = pendingStreamStarts.first {
|
|
1104
|
+
return (entry.key, entry.value)
|
|
1105
|
+
}
|
|
1106
|
+
return nil
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
private func streamStatus(_ event: StreamStatusEvent, matches streamId: String?) -> Bool {
|
|
1110
|
+
guard let streamId, !streamId.isEmpty else { return true }
|
|
1111
|
+
guard let eventStreamId = event.streamId, !eventStreamId.isEmpty else { return true }
|
|
1112
|
+
return eventStreamId == streamId
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private func isAlreadyStoppedStreamStatus(_ event: StreamStatusEvent) -> Bool {
|
|
1116
|
+
if event.state == .stopped {
|
|
1117
|
+
return true
|
|
1118
|
+
}
|
|
1119
|
+
guard case let .error(_, errorDetails, _, _) = event.status else {
|
|
1120
|
+
return false
|
|
1121
|
+
}
|
|
1122
|
+
return ["not_streaming", "already_stopped", "not streaming"].contains(errorDetails.lowercased())
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private func stoppedStreamEvent(from event: StreamStatusEvent, fallbackStreamId: String?) -> StreamStatusEvent {
|
|
1126
|
+
StreamStatusEvent(
|
|
1127
|
+
status: .lifecycle(
|
|
1128
|
+
state: .stopped,
|
|
1129
|
+
streamId: event.streamId ?? fallbackStreamId,
|
|
1130
|
+
timestamp: event.status.timestamp ?? Int(Date().timeIntervalSince1970 * 1000),
|
|
1131
|
+
resolvedConfig: event.resolvedConfig
|
|
1132
|
+
)
|
|
1133
|
+
)
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private func streamStatusError(_ event: StreamStatusEvent, code: String) -> BluetoothError {
|
|
1137
|
+
let message: String
|
|
1138
|
+
if case let .error(_, errorDetails, _, _) = event.status {
|
|
1139
|
+
message = errorDetails
|
|
1140
|
+
} else {
|
|
1141
|
+
message = "Stream status \(event.state.rawValue)"
|
|
1142
|
+
}
|
|
1143
|
+
return BluetoothError(code: code, message: message)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
private func handlePhotoResponseForRequests(_ event: PhotoResponseEvent) {
|
|
1147
|
+
guard let pending = pendingPhotoRequests[event.requestId] else { return }
|
|
1148
|
+
switch event.response {
|
|
1149
|
+
case .success:
|
|
1150
|
+
pending.resolve(event)
|
|
1151
|
+
case let .error(_, errorCode, errorMessage, _):
|
|
1152
|
+
pending.reject(
|
|
1153
|
+
BluetoothError(
|
|
1154
|
+
code: errorCode ?? "photo_request_failed",
|
|
1155
|
+
message: errorMessage
|
|
1156
|
+
)
|
|
1157
|
+
)
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
private func handleVideoRecordingStatusForRequests(_ event: VideoRecordingStatusEvent) {
|
|
1162
|
+
guard let request = pendingVideoRecordingRequests[event.requestId] else { return }
|
|
1163
|
+
if event.success {
|
|
1164
|
+
if event.status == request.expectedStatus {
|
|
1165
|
+
request.pending.resolve(event)
|
|
1166
|
+
}
|
|
1167
|
+
} else {
|
|
1168
|
+
request.pending.reject(
|
|
1169
|
+
BluetoothError(
|
|
1170
|
+
code: event.status.isEmpty ? "video_recording_failed" : event.status,
|
|
1171
|
+
message: event.details ?? "Video recording command failed."
|
|
1172
|
+
)
|
|
1173
|
+
)
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
private func handleRgbLedResponseForRequests(_ event: RgbLedControlResponseEvent) {
|
|
1178
|
+
guard let pending = pendingRgbLedRequests[event.requestId] else { return }
|
|
1179
|
+
if event.state == "success" {
|
|
1180
|
+
pending.resolve(event)
|
|
1181
|
+
} else {
|
|
1182
|
+
pending.reject(
|
|
1183
|
+
BluetoothError(
|
|
1184
|
+
code: event.errorCode ?? "rgb_led_control_failed",
|
|
1185
|
+
message: event.errorCode ?? "RGB LED command failed."
|
|
1186
|
+
)
|
|
1187
|
+
)
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
private func handleSettingsAckForRequests(_ event: SettingsAckEvent) {
|
|
1192
|
+
guard let pending = pendingSettingsRequests[event.requestId] else { return }
|
|
1193
|
+
if isFailureStatus(event.status) {
|
|
1194
|
+
let fallbackSetting = event.setting.isEmpty ? event.requestId : event.setting
|
|
1195
|
+
pending.reject(
|
|
1196
|
+
BluetoothError(
|
|
1197
|
+
code: event.errorCode ?? "\(event.setting.isEmpty ? "settings" : event.setting)_failed",
|
|
1198
|
+
message: event.errorMessage ?? "Settings command \(fallbackSetting) failed."
|
|
1199
|
+
)
|
|
1200
|
+
)
|
|
1201
|
+
} else {
|
|
1202
|
+
pending.resolve(event)
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
private func isFailureStatus(_ status: String) -> Bool {
|
|
1207
|
+
["error", "failed", "failure", "rejected"].contains(status.lowercased())
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
private func handleWifiScanResultsForRequests(_ results: [WifiScanResult]) {
|
|
1211
|
+
guard let request = pendingWifiScan else { return }
|
|
1212
|
+
if pendingWifiScan === request {
|
|
1213
|
+
pendingWifiScan = nil
|
|
1214
|
+
}
|
|
1215
|
+
request.pending.resolve(results)
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
private func handleWifiStatusForRequests(_ event: WifiStatusEvent) {
|
|
1219
|
+
guard let request = pendingWifiStatus else { return }
|
|
1220
|
+
guard wifiStatusMatches(event.status, request: request) else { return }
|
|
1221
|
+
if pendingWifiStatus === request {
|
|
1222
|
+
pendingWifiStatus = nil
|
|
1223
|
+
}
|
|
1224
|
+
request.pending.resolve(event)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
private func wifiStatusMatches(_ status: WifiStatus, request: PendingWifiStatusRequest) -> Bool {
|
|
1228
|
+
switch request.operation {
|
|
1229
|
+
case .connect:
|
|
1230
|
+
if case let .connected(ssid, _) = status {
|
|
1231
|
+
return ssid == request.ssid
|
|
1232
|
+
}
|
|
1233
|
+
return false
|
|
1234
|
+
case .forget:
|
|
1235
|
+
switch status {
|
|
1236
|
+
case .disconnected:
|
|
1237
|
+
return true
|
|
1238
|
+
case let .connected(ssid, _):
|
|
1239
|
+
return ssid != request.ssid
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
private func handleHotspotStatusForRequests(_ event: HotspotStatusEvent) {
|
|
1245
|
+
guard let request = pendingHotspotStatus else { return }
|
|
1246
|
+
guard hotspotStatusMatches(event.status, enabled: request.enabled) else { return }
|
|
1247
|
+
if pendingHotspotStatus === request {
|
|
1248
|
+
pendingHotspotStatus = nil
|
|
1249
|
+
}
|
|
1250
|
+
request.pending.resolve(event)
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private func hotspotStatusMatches(_ status: HotspotStatus, enabled: Bool) -> Bool {
|
|
1254
|
+
if enabled {
|
|
1255
|
+
return status.isEnabled
|
|
1256
|
+
}
|
|
1257
|
+
return status == .disabled
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
private func handleHotspotErrorForRequests(_ event: HotspotErrorEvent) {
|
|
1261
|
+
guard let request = pendingHotspotStatus else { return }
|
|
1262
|
+
if pendingHotspotStatus === request {
|
|
1263
|
+
pendingHotspotStatus = nil
|
|
1264
|
+
}
|
|
1265
|
+
request.pending.reject(
|
|
1266
|
+
BluetoothError(
|
|
1267
|
+
code: "hotspot_command_failed",
|
|
1268
|
+
message: event.message ?? "Hotspot command failed."
|
|
1269
|
+
)
|
|
1270
|
+
)
|
|
1271
|
+
}
|
|
1272
|
+
|
|
570
1273
|
private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
|
|
571
1274
|
switch ObservableStore.normalizeCategory(category) {
|
|
572
1275
|
case "glasses":
|
|
@@ -688,17 +1391,46 @@ public final class MentraBluetoothSDK {
|
|
|
688
1391
|
didReceive: .speakingStatus(SpeakingStatusEvent(values: data))
|
|
689
1392
|
)
|
|
690
1393
|
case "hotspot_status_change":
|
|
691
|
-
|
|
1394
|
+
let event = HotspotStatusEvent(values: data)
|
|
1395
|
+
handleHotspotStatusForRequests(event)
|
|
1396
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .hotspotStatus(event))
|
|
692
1397
|
case "wifi_status_change":
|
|
693
|
-
|
|
1398
|
+
let event = WifiStatusEvent(values: data)
|
|
1399
|
+
handleWifiStatusForRequests(event)
|
|
1400
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .wifiStatus(event))
|
|
1401
|
+
case "wifi_scan_result":
|
|
1402
|
+
let networks = (data["networks"] as? [[String: Any]])?.map(WifiScanResult.init(values:)) ?? []
|
|
1403
|
+
let hasCompletionFlag = data.keys.contains("scanComplete") || data.keys.contains("scan_complete")
|
|
1404
|
+
let scanComplete = data["scanComplete"] as? Bool ?? data["scan_complete"] as? Bool ?? false
|
|
1405
|
+
if scanComplete || !hasCompletionFlag {
|
|
1406
|
+
handleWifiScanResultsForRequests(networks)
|
|
1407
|
+
}
|
|
1408
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: "wifi_scan_result", values: data))
|
|
694
1409
|
case "hotspot_error":
|
|
695
|
-
|
|
1410
|
+
let event = HotspotErrorEvent(values: data)
|
|
1411
|
+
handleHotspotErrorForRequests(event)
|
|
1412
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .hotspotError(event))
|
|
1413
|
+
case "gallery_status":
|
|
1414
|
+
let event = GalleryStatusEvent(values: data)
|
|
1415
|
+
pendingGalleryStatus?.resolve(event)
|
|
1416
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: "gallery_status", values: event.values))
|
|
696
1417
|
case "photo_response":
|
|
697
|
-
|
|
1418
|
+
let event = PhotoResponseEvent(values: data)
|
|
1419
|
+
handlePhotoResponseForRequests(event)
|
|
1420
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .photoResponse(event))
|
|
698
1421
|
case "photo_status":
|
|
699
1422
|
delegate?.mentraBluetoothSDK(self, didReceive: .photoStatus(PhotoStatusEvent(values: data)))
|
|
1423
|
+
case "video_recording_status":
|
|
1424
|
+
let event = VideoRecordingStatusEvent(values: data)
|
|
1425
|
+
handleVideoRecordingStatusForRequests(event)
|
|
1426
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .videoRecordingStatus(event))
|
|
1427
|
+
case "rgb_led_control_response":
|
|
1428
|
+
let event = RgbLedControlResponseEvent(values: data)
|
|
1429
|
+
handleRgbLedResponseForRequests(event)
|
|
1430
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .rgbLedControlResponse(event))
|
|
700
1431
|
case "stream_status":
|
|
701
1432
|
let event = StreamStatusEvent(values: data)
|
|
1433
|
+
handleStreamStatusForRequests(event)
|
|
702
1434
|
handleStreamStatusForKeepAlive(event.status)
|
|
703
1435
|
delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(event))
|
|
704
1436
|
case "keep_alive_ack":
|
|
@@ -706,6 +1438,30 @@ public final class MentraBluetoothSDK {
|
|
|
706
1438
|
if !handleStreamKeepAliveAck(event) {
|
|
707
1439
|
delegate?.mentraBluetoothSDK(self, didReceive: .keepAliveAck(event))
|
|
708
1440
|
}
|
|
1441
|
+
case "ota_update_available":
|
|
1442
|
+
var resultValues = data
|
|
1443
|
+
resultValues["type"] = "ota_update_available"
|
|
1444
|
+
pendingOtaQuery?.resolve(OtaQueryResult(values: resultValues))
|
|
1445
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .otaUpdateAvailable(OtaUpdateAvailableEvent(values: resultValues)))
|
|
1446
|
+
case "ota_start_ack":
|
|
1447
|
+
var values = data
|
|
1448
|
+
values["type"] = "ota_start_ack"
|
|
1449
|
+
let event = OtaStartAckEvent(values: values)
|
|
1450
|
+
pendingOtaStart?.resolve(event)
|
|
1451
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .otaStartAck(event))
|
|
1452
|
+
case "ota_status":
|
|
1453
|
+
var resultValues = data
|
|
1454
|
+
resultValues["type"] = "ota_status"
|
|
1455
|
+
pendingOtaQuery?.resolve(OtaQueryResult(values: resultValues))
|
|
1456
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .otaStatus(OtaStatusEvent(values: resultValues)))
|
|
1457
|
+
case "settings_ack":
|
|
1458
|
+
let event = SettingsAckEvent(values: data)
|
|
1459
|
+
handleSettingsAckForRequests(event)
|
|
1460
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .settingsAck(event))
|
|
1461
|
+
case "version_info":
|
|
1462
|
+
let event = VersionInfoResult(values: data)
|
|
1463
|
+
pendingVersionInfo?.resolve(event)
|
|
1464
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .versionInfo(event))
|
|
709
1465
|
case "compatible_glasses_search_stop":
|
|
710
1466
|
delegate?.mentraBluetoothSDK(self, didStopScan: .completed)
|
|
711
1467
|
case "pair_failure":
|