@mentra/bluetooth-sdk 3.2.0-dev.200 → 3.2.0-dev.206
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 +46 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +63 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +5 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +298 -181
- package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +326 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbe.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamSessionState.kt +32 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbeTest.kt +24 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamSessionStateTest.kt +43 -0
- package/build/BluetoothSdk.types.d.ts +68 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +3 -4
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +8 -13
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +63 -0
- package/ios/Source/DeviceManager.swift +6 -3
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +372 -220
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/sgcs/MentraLive.swift +216 -70
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +28 -0
- package/ios/Source/streaming/StreamControllerProbe.swift +18 -0
- package/ios/Source/streaming/StreamModels.swift +12 -0
- package/ios/Source/streaming/StreamSessionState.swift +29 -0
- package/ios/Tests/StreamControllerProbeTests.swift +27 -0
- package/ios/Tests/StreamSessionStateTests.swift +38 -0
- package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +82 -2
- package/src/_private/BluetoothSdkModule.ts +4 -4
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +11 -0
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
import CoreBluetooth
|
|
2
2
|
import Foundation
|
|
3
3
|
|
|
4
|
-
@MainActor
|
|
5
|
-
private final class ActiveStreamKeepAlive {
|
|
6
|
-
let streamId: String
|
|
7
|
-
let intervalSeconds: Int
|
|
8
|
-
var pendingAckId: String?
|
|
9
|
-
var missedAckCount = 0
|
|
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
|
|
15
|
-
|
|
16
|
-
init(streamId: String, intervalSeconds: Int) {
|
|
17
|
-
self.streamId = streamId
|
|
18
|
-
self.intervalSeconds = intervalSeconds
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
4
|
@MainActor
|
|
23
5
|
private final class ActiveScanSession {
|
|
24
6
|
let model: DeviceModel
|
|
@@ -47,8 +29,8 @@ private final class PendingWifiScan {
|
|
|
47
29
|
let pending: PendingResponse<[WifiScanResult]>
|
|
48
30
|
let scanId: String
|
|
49
31
|
var latestResults: [WifiScanResult] = []
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
/// Chunks accumulated from scanId-echoing glasses, deduplicated by SSID;
|
|
33
|
+
/// resolved only when the glasses flag the scan complete.
|
|
52
34
|
var accumulated: [WifiScanResult] = []
|
|
53
35
|
|
|
54
36
|
init(pending: PendingResponse<[WifiScanResult]>, scanId: String) {
|
|
@@ -57,21 +39,68 @@ private final class PendingWifiScan {
|
|
|
57
39
|
}
|
|
58
40
|
}
|
|
59
41
|
|
|
60
|
-
private enum WifiStatusOperation {
|
|
61
|
-
case connect
|
|
62
|
-
case forget
|
|
63
|
-
}
|
|
64
|
-
|
|
65
42
|
@MainActor
|
|
66
43
|
private final class PendingWifiStatusRequest {
|
|
67
|
-
let operation: WifiStatusOperation
|
|
68
44
|
let ssid: String
|
|
69
45
|
let pending: PendingResponse<WifiStatusEvent>
|
|
70
46
|
|
|
71
|
-
init(
|
|
72
|
-
|
|
47
|
+
init(
|
|
48
|
+
ssid: String,
|
|
49
|
+
pending: PendingResponse<WifiStatusEvent>
|
|
50
|
+
) {
|
|
51
|
+
self.ssid = ssid
|
|
52
|
+
self.pending = pending
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@MainActor
|
|
57
|
+
private final class PendingWifiForgetRequest {
|
|
58
|
+
let ssid: String
|
|
59
|
+
let requestId: String
|
|
60
|
+
var sid: String
|
|
61
|
+
let epoch: UInt64
|
|
62
|
+
let pending: PendingResponse<WifiForgetResult>
|
|
63
|
+
var mode: WifiRequestMode
|
|
64
|
+
var commandSent = false
|
|
65
|
+
|
|
66
|
+
init(
|
|
67
|
+
ssid: String,
|
|
68
|
+
requestId: String,
|
|
69
|
+
sid: String,
|
|
70
|
+
epoch: UInt64,
|
|
71
|
+
pending: PendingResponse<WifiForgetResult>,
|
|
72
|
+
mode: WifiRequestMode
|
|
73
|
+
) {
|
|
73
74
|
self.ssid = ssid
|
|
75
|
+
self.requestId = requestId
|
|
76
|
+
self.sid = sid
|
|
77
|
+
self.epoch = epoch
|
|
78
|
+
self.pending = pending
|
|
79
|
+
self.mode = mode
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@MainActor
|
|
84
|
+
private final class PendingSavedWifiNetworks {
|
|
85
|
+
let requestId: String
|
|
86
|
+
var sid: String
|
|
87
|
+
let epoch: UInt64
|
|
88
|
+
let pending: PendingResponse<SavedWifiNetworksResult>
|
|
89
|
+
var mode: WifiRequestMode
|
|
90
|
+
var commandSent = false
|
|
91
|
+
|
|
92
|
+
init(
|
|
93
|
+
requestId: String,
|
|
94
|
+
sid: String,
|
|
95
|
+
epoch: UInt64,
|
|
96
|
+
pending: PendingResponse<SavedWifiNetworksResult>,
|
|
97
|
+
mode: WifiRequestMode
|
|
98
|
+
) {
|
|
99
|
+
self.requestId = requestId
|
|
100
|
+
self.sid = sid
|
|
101
|
+
self.epoch = epoch
|
|
74
102
|
self.pending = pending
|
|
103
|
+
self.mode = mode
|
|
75
104
|
}
|
|
76
105
|
}
|
|
77
106
|
|
|
@@ -112,17 +141,17 @@ private final class PendingVersionInfoRequest {
|
|
|
112
141
|
}
|
|
113
142
|
}
|
|
114
143
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
/// seq records send order (assigned and handed to the BLE queue in a single
|
|
145
|
+
/// MainActor turn) so that an id-carrying status for a newer start can
|
|
146
|
+
/// identify which older in-flight starts it preempted.
|
|
118
147
|
@MainActor
|
|
119
148
|
private final class PendingStreamStart {
|
|
120
149
|
let seq: Int
|
|
121
150
|
let pending: PendingResponse<StreamStatusEvent>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
151
|
+
/// Set when an id-carrying error arrives: a fatal publisher error never
|
|
152
|
+
/// reaches the reconnect machinery and winds down with a streamId-less
|
|
153
|
+
/// stopped, so the stash both attributes that stopped to this start and
|
|
154
|
+
/// preserves the real error details for the rejection.
|
|
126
155
|
var lastError: StreamStatusEvent?
|
|
127
156
|
|
|
128
157
|
init(seq: Int, pending: PendingResponse<StreamStatusEvent>) {
|
|
@@ -132,7 +161,7 @@ private final class PendingStreamStart {
|
|
|
132
161
|
}
|
|
133
162
|
|
|
134
163
|
@MainActor
|
|
135
|
-
|
|
164
|
+
final class PendingResponse<T> {
|
|
136
165
|
private let operation: String
|
|
137
166
|
private var continuation: CheckedContinuation<T, Error>?
|
|
138
167
|
private var timeoutTask: Task<Void, Never>?
|
|
@@ -161,22 +190,39 @@ private final class PendingResponse<T> {
|
|
|
161
190
|
continuation = nil
|
|
162
191
|
}
|
|
163
192
|
|
|
164
|
-
func wait(timeoutMs: Int =
|
|
193
|
+
func wait(timeoutMs: Int? = 15000) async throws -> T {
|
|
194
|
+
if Task.isCancelled {
|
|
195
|
+
throw BluetoothSdkError(code: "request_cancelled", message: "\(operation) was cancelled.")
|
|
196
|
+
}
|
|
165
197
|
if let result {
|
|
166
198
|
return try result.get()
|
|
167
199
|
}
|
|
168
|
-
return try await
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
200
|
+
return try await withTaskCancellationHandler {
|
|
201
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
202
|
+
self.continuation = continuation
|
|
203
|
+
if let timeoutMs {
|
|
204
|
+
timeoutTask = Task { @MainActor [weak self] in
|
|
205
|
+
do {
|
|
206
|
+
try await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000)
|
|
207
|
+
} catch {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
self?.reject(
|
|
211
|
+
BluetoothSdkError(
|
|
212
|
+
code: "request_timeout",
|
|
213
|
+
message: "\(self?.operation ?? "Request") timed out waiting for glasses response."
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
}
|
|
175
217
|
}
|
|
176
|
-
|
|
218
|
+
}
|
|
219
|
+
} onCancel: {
|
|
220
|
+
Task { @MainActor [weak self] in
|
|
221
|
+
guard let self else { return }
|
|
222
|
+
reject(
|
|
177
223
|
BluetoothSdkError(
|
|
178
|
-
code: "
|
|
179
|
-
message: "\(
|
|
224
|
+
code: "request_cancelled",
|
|
225
|
+
message: "\(operation) was cancelled."
|
|
180
226
|
)
|
|
181
227
|
)
|
|
182
228
|
}
|
|
@@ -186,14 +232,13 @@ private final class PendingResponse<T> {
|
|
|
186
232
|
|
|
187
233
|
@MainActor
|
|
188
234
|
public final class MentraBluetoothSDK {
|
|
189
|
-
private static let wifiScanTimeoutMs =
|
|
235
|
+
private static let wifiScanTimeoutMs = 20000
|
|
190
236
|
// A photo response is terminal only after capture, encoding, transport, and upload.
|
|
191
237
|
// Max-quality BLE fallback can legitimately exceed the generic command deadline.
|
|
192
|
-
private static let photoRequestTimeoutMs =
|
|
193
|
-
private static let otaBesVersionWaitMs =
|
|
194
|
-
private static let otaMtkVersionWaitMs =
|
|
238
|
+
private static let photoRequestTimeoutMs = 30000
|
|
239
|
+
private static let otaBesVersionWaitMs = 5000
|
|
240
|
+
private static let otaMtkVersionWaitMs = 2000
|
|
195
241
|
private static let otaVersionPollMs = 100
|
|
196
|
-
private static let defaultStreamKeepAliveIntervalSeconds = 5
|
|
197
242
|
|
|
198
243
|
public weak var delegate: MentraBluetoothSDKDelegate?
|
|
199
244
|
|
|
@@ -210,7 +255,7 @@ public final class MentraBluetoothSDK {
|
|
|
210
255
|
private var suppressDefaultDeviceEvents = false
|
|
211
256
|
private var defaultDeviceApplyGeneration = 0
|
|
212
257
|
private var activeScanSessions: [UUID: ActiveScanSession] = [:]
|
|
213
|
-
private
|
|
258
|
+
private let streamSession = StreamSessionState()
|
|
214
259
|
private let analytics: BluetoothSdkAnalytics
|
|
215
260
|
private var pendingPhotoRequests: [String: PendingResponse<PhotoResponseEvent>] = [:]
|
|
216
261
|
private var pendingCameraStatusRequests: [String: PendingResponse<CameraStatusEvent>] = [:]
|
|
@@ -227,10 +272,13 @@ public final class MentraBluetoothSDK {
|
|
|
227
272
|
private var pendingOtaQuery: PendingResponse<OtaQueryResult>?
|
|
228
273
|
private var pendingOtaStart: PendingResponse<OtaStartAckEvent>?
|
|
229
274
|
private var pendingWifiScan: PendingWifiScan?
|
|
275
|
+
private var pendingSavedWifiNetworks: PendingSavedWifiNetworks?
|
|
230
276
|
private var wifiScanTask: Task<[WifiScanResult], Error>?
|
|
231
277
|
private var pendingWifiStatus: PendingWifiStatusRequest?
|
|
278
|
+
private var pendingWifiForget: PendingWifiForgetRequest?
|
|
232
279
|
private var pendingHotspotStatus: PendingHotspotStatusRequest?
|
|
233
280
|
private var pendingVersionInfo: PendingVersionInfoRequest?
|
|
281
|
+
private let wifiSessionCapabilities = WifiSessionCapabilities()
|
|
234
282
|
private var configuredOtaVersionUrl: String?
|
|
235
283
|
|
|
236
284
|
public init(configuration: MentraBluetoothSDKConfiguration = .default) {
|
|
@@ -620,7 +668,8 @@ public final class MentraBluetoothSDK {
|
|
|
620
668
|
for key in ["button_photo_zsl_mfnr", "button_photo_mfnr", "button_photo_zsl", "button_photo_noise_reduction",
|
|
621
669
|
"button_photo_edge_enhancement", "button_photo_isp_digital_gain",
|
|
622
670
|
"button_photo_isp_analog_gain", "button_photo_ae_exposure_divisor",
|
|
623
|
-
"button_photo_iso_cap", "button_photo_compress", "button_photo_sound"]
|
|
671
|
+
"button_photo_iso_cap", "button_photo_compress", "button_photo_sound"]
|
|
672
|
+
{
|
|
624
673
|
DeviceStore.shared.remove(cat, key)
|
|
625
674
|
}
|
|
626
675
|
}
|
|
@@ -874,7 +923,8 @@ public final class MentraBluetoothSDK {
|
|
|
874
923
|
return try await pending.wait(timeoutMs: MentraBluetoothSDK.wifiScanTimeoutMs)
|
|
875
924
|
} catch {
|
|
876
925
|
if (error as? BluetoothSdkError)?.code == "request_timeout",
|
|
877
|
-
!request.latestResults.isEmpty
|
|
926
|
+
!request.latestResults.isEmpty
|
|
927
|
+
{
|
|
878
928
|
return request.latestResults
|
|
879
929
|
}
|
|
880
930
|
throw error
|
|
@@ -885,14 +935,14 @@ public final class MentraBluetoothSDK {
|
|
|
885
935
|
}
|
|
886
936
|
|
|
887
937
|
public func sendWifiCredentials(ssid: String, password: String) async throws -> WifiStatusEvent {
|
|
888
|
-
guard pendingWifiStatus == nil else {
|
|
938
|
+
guard pendingWifiStatus == nil, pendingWifiForget == nil else {
|
|
889
939
|
throw BluetoothSdkError(
|
|
890
940
|
code: "request_in_flight",
|
|
891
941
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
892
942
|
)
|
|
893
943
|
}
|
|
894
944
|
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi connect request")
|
|
895
|
-
pendingWifiStatus = PendingWifiStatusRequest(
|
|
945
|
+
pendingWifiStatus = PendingWifiStatusRequest(ssid: ssid, pending: pending)
|
|
896
946
|
DeviceManager.shared.sendWifiCredentials(ssid, password)
|
|
897
947
|
do {
|
|
898
948
|
let event = try await pending.wait()
|
|
@@ -908,25 +958,83 @@ public final class MentraBluetoothSDK {
|
|
|
908
958
|
}
|
|
909
959
|
}
|
|
910
960
|
|
|
911
|
-
public func forgetWifiNetwork(ssid: String) async throws ->
|
|
912
|
-
guard
|
|
961
|
+
public func forgetWifiNetwork(ssid: String) async throws -> WifiForgetResult {
|
|
962
|
+
guard wifiSsidIsValid(ssid) else {
|
|
963
|
+
throw BluetoothSdkError(code: "invalid_ssid", message: "WiFi SSID cannot be empty.")
|
|
964
|
+
}
|
|
965
|
+
guard pendingWifiStatus == nil, pendingWifiForget == nil else {
|
|
913
966
|
throw BluetoothSdkError(
|
|
914
967
|
code: "request_in_flight",
|
|
915
968
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
916
969
|
)
|
|
917
970
|
}
|
|
918
|
-
let pending = PendingResponse<
|
|
919
|
-
|
|
920
|
-
|
|
971
|
+
let pending = PendingResponse<WifiForgetResult>(operation: "WiFi forget request")
|
|
972
|
+
let requestId = "forget-\(UUID().uuidString)"
|
|
973
|
+
let request = PendingWifiForgetRequest(
|
|
974
|
+
ssid: ssid,
|
|
975
|
+
requestId: requestId,
|
|
976
|
+
sid: wifiSessionCapabilities.sessionId,
|
|
977
|
+
epoch: wifiSessionCapabilities.epoch,
|
|
978
|
+
pending: pending,
|
|
979
|
+
mode: wifiSessionCapabilities.forgetMode()
|
|
980
|
+
)
|
|
981
|
+
pendingWifiForget = request
|
|
982
|
+
dispatchWifiForgetIfReady(request)
|
|
921
983
|
do {
|
|
922
984
|
let event = try await pending.wait()
|
|
923
|
-
if
|
|
924
|
-
|
|
985
|
+
if pendingWifiForget === request {
|
|
986
|
+
pendingWifiForget = nil
|
|
925
987
|
}
|
|
926
988
|
return event
|
|
927
989
|
} catch {
|
|
928
|
-
if
|
|
929
|
-
|
|
990
|
+
if pendingWifiForget === request {
|
|
991
|
+
pendingWifiForget = nil
|
|
992
|
+
}
|
|
993
|
+
if let sdkError = error as? BluetoothSdkError, sdkError.code == "request_timeout", request.mode == .discovering {
|
|
994
|
+
throw BluetoothSdkError(code: wifiCapabilityNegotiationTimeoutCode, message: "WiFi capability negotiation timed out.")
|
|
995
|
+
}
|
|
996
|
+
throw error
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
public func getSavedWifiNetworks() async throws -> SavedWifiNetworksResult {
|
|
1001
|
+
guard pendingSavedWifiNetworks == nil else {
|
|
1002
|
+
throw BluetoothSdkError(
|
|
1003
|
+
code: "request_in_flight",
|
|
1004
|
+
message: "A saved WiFi networks request is already waiting for a glasses response."
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
1007
|
+
let requestId = "saved-\(UUID().uuidString)"
|
|
1008
|
+
let mode = wifiSessionCapabilities.savedNetworksMode()
|
|
1009
|
+
if mode == .legacy || mode == .unsupported {
|
|
1010
|
+
return SavedWifiNetworksResult(
|
|
1011
|
+
outcome: .unsupported,
|
|
1012
|
+
networks: [],
|
|
1013
|
+
error: "saved_wifi_networks_unsupported"
|
|
1014
|
+
)
|
|
1015
|
+
}
|
|
1016
|
+
let pending = PendingResponse<SavedWifiNetworksResult>(operation: "Saved WiFi networks request")
|
|
1017
|
+
let request = PendingSavedWifiNetworks(
|
|
1018
|
+
requestId: requestId,
|
|
1019
|
+
sid: wifiSessionCapabilities.sessionId,
|
|
1020
|
+
epoch: wifiSessionCapabilities.epoch,
|
|
1021
|
+
pending: pending,
|
|
1022
|
+
mode: mode
|
|
1023
|
+
)
|
|
1024
|
+
pendingSavedWifiNetworks = request
|
|
1025
|
+
dispatchSavedWifiNetworksIfReady(request)
|
|
1026
|
+
do {
|
|
1027
|
+
let networks = try await pending.wait()
|
|
1028
|
+
if pendingSavedWifiNetworks === request {
|
|
1029
|
+
pendingSavedWifiNetworks = nil
|
|
1030
|
+
}
|
|
1031
|
+
return networks
|
|
1032
|
+
} catch {
|
|
1033
|
+
if pendingSavedWifiNetworks === request {
|
|
1034
|
+
pendingSavedWifiNetworks = nil
|
|
1035
|
+
}
|
|
1036
|
+
if let sdkError = error as? BluetoothSdkError, sdkError.code == "request_timeout", request.mode == .discovering {
|
|
1037
|
+
throw BluetoothSdkError(code: wifiCapabilityNegotiationTimeoutCode, message: "WiFi capability negotiation timed out.")
|
|
930
1038
|
}
|
|
931
1039
|
throw error
|
|
932
1040
|
}
|
|
@@ -1054,14 +1162,10 @@ public final class MentraBluetoothSDK {
|
|
|
1054
1162
|
}
|
|
1055
1163
|
|
|
1056
1164
|
public func startStream(_ request: StreamRequest) async throws -> StreamStatusEvent {
|
|
1057
|
-
try
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
try await startStream(request, startSdkKeepAlive: false)
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
private func startStream(_ request: StreamRequest, startSdkKeepAlive: Bool) async throws -> StreamStatusEvent {
|
|
1165
|
+
try requireGlassesConnected(operation: "start stream")
|
|
1166
|
+
guard streamSession.supported else {
|
|
1167
|
+
throw BluetoothSdkError(code: "stream_control_unsupported", message: "Update the glasses software before starting a stream.")
|
|
1168
|
+
}
|
|
1065
1169
|
var values = request.values
|
|
1066
1170
|
let streamId = stringValue(values, "streamId").flatMap { $0.isEmpty ? nil : $0 } ?? "sdk-\(UUID().uuidString)"
|
|
1067
1171
|
values["streamId"] = streamId
|
|
@@ -1072,17 +1176,10 @@ public final class MentraBluetoothSDK {
|
|
|
1072
1176
|
// glasses' FIFO.
|
|
1073
1177
|
streamStartSeq += 1
|
|
1074
1178
|
pendingStreamStarts[streamId] = PendingStreamStart(seq: streamStartSeq, pending: pending)
|
|
1075
|
-
stopStreamKeepAliveMonitor()
|
|
1076
1179
|
DeviceManager.shared.startStream(values)
|
|
1077
1180
|
do {
|
|
1078
|
-
let event = try await pending.wait(timeoutMs:
|
|
1181
|
+
let event = try await pending.wait(timeoutMs: 30000)
|
|
1079
1182
|
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1080
|
-
if startSdkKeepAlive {
|
|
1081
|
-
startStreamKeepAliveMonitor(
|
|
1082
|
-
streamId: streamId,
|
|
1083
|
-
intervalSeconds: Self.defaultStreamKeepAliveIntervalSeconds
|
|
1084
|
-
)
|
|
1085
|
-
}
|
|
1086
1183
|
return event
|
|
1087
1184
|
} catch {
|
|
1088
1185
|
pendingStreamStarts.removeValue(forKey: streamId)
|
|
@@ -1090,10 +1187,6 @@ public final class MentraBluetoothSDK {
|
|
|
1090
1187
|
}
|
|
1091
1188
|
}
|
|
1092
1189
|
|
|
1093
|
-
func sendExternallyManagedStreamKeepAlive(_ request: StreamKeepAliveRequest) {
|
|
1094
|
-
DeviceManager.shared.keepStreamAlive(request.values)
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
1190
|
public func rgbLedControl(_ request: RgbLedRequest) async throws -> RgbLedControlResponseEvent {
|
|
1098
1191
|
let pending = PendingResponse<RgbLedControlResponseEvent>(operation: "RGB LED command \(request.requestId)")
|
|
1099
1192
|
pendingRgbLedRequests[request.requestId] = pending
|
|
@@ -1130,11 +1223,10 @@ public final class MentraBluetoothSDK {
|
|
|
1130
1223
|
// separate the pending starts the glasses consumed before it (lower
|
|
1131
1224
|
// seq) from starts sent after it (higher seq).
|
|
1132
1225
|
streamStartSeq += 1
|
|
1133
|
-
pendingStreamStop = (streamId:
|
|
1134
|
-
stopStreamKeepAliveMonitor()
|
|
1226
|
+
pendingStreamStop = (streamId: streamSession.currentStreamId, seq: streamStartSeq, pending: pending)
|
|
1135
1227
|
DeviceManager.shared.stopStream()
|
|
1136
1228
|
do {
|
|
1137
|
-
let event = try await pending.wait(timeoutMs:
|
|
1229
|
+
let event = try await pending.wait(timeoutMs: 15000)
|
|
1138
1230
|
if pendingStreamStop?.pending === pending {
|
|
1139
1231
|
pendingStreamStop = nil
|
|
1140
1232
|
}
|
|
@@ -1206,7 +1298,7 @@ public final class MentraBluetoothSDK {
|
|
|
1206
1298
|
)
|
|
1207
1299
|
DeviceManager.shared.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1208
1300
|
do {
|
|
1209
|
-
let timeoutMs = waitForUpload ? videoUploadStopTimeoutMs :
|
|
1301
|
+
let timeoutMs = waitForUpload ? videoUploadStopTimeoutMs : 15000
|
|
1210
1302
|
let event = try await pending.wait(timeoutMs: timeoutMs)
|
|
1211
1303
|
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
1212
1304
|
return event
|
|
@@ -1397,7 +1489,9 @@ public final class MentraBluetoothSDK {
|
|
|
1397
1489
|
try await startOtaCommand(otaVersionUrl: otaVersionUrl)
|
|
1398
1490
|
}
|
|
1399
1491
|
|
|
1400
|
-
func sendOtaQueryStatus() async throws -> OtaQueryResult {
|
|
1492
|
+
func sendOtaQueryStatus() async throws -> OtaQueryResult {
|
|
1493
|
+
try await queryOtaStatus()
|
|
1494
|
+
}
|
|
1401
1495
|
|
|
1402
1496
|
func startAr99OtaFromFile(_ path: String) throws -> Bool {
|
|
1403
1497
|
try requireGlassesConnected(operation: "start AR99 OTA")
|
|
@@ -1461,7 +1555,7 @@ public final class MentraBluetoothSDK {
|
|
|
1461
1555
|
timeoutMs: Int,
|
|
1462
1556
|
isReady: (GlassesStatus) -> Bool
|
|
1463
1557
|
) async -> GlassesStatus {
|
|
1464
|
-
let deadline = Date().addingTimeInterval(Double(timeoutMs) /
|
|
1558
|
+
let deadline = Date().addingTimeInterval(Double(timeoutMs) / 1000)
|
|
1465
1559
|
var status = initialStatus
|
|
1466
1560
|
while Date() < deadline {
|
|
1467
1561
|
status = glassesStatus
|
|
@@ -1469,7 +1563,7 @@ public final class MentraBluetoothSDK {
|
|
|
1469
1563
|
return status
|
|
1470
1564
|
}
|
|
1471
1565
|
|
|
1472
|
-
let remainingMs = max(0, Int(deadline.timeIntervalSinceNow *
|
|
1566
|
+
let remainingMs = max(0, Int(deadline.timeIntervalSinceNow * 1000))
|
|
1473
1567
|
let sleepMs = min(Self.otaVersionPollMs, remainingMs)
|
|
1474
1568
|
if sleepMs <= 0 {
|
|
1475
1569
|
break
|
|
@@ -1510,7 +1604,7 @@ public final class MentraBluetoothSDK {
|
|
|
1510
1604
|
}
|
|
1511
1605
|
|
|
1512
1606
|
public func invalidate() {
|
|
1513
|
-
|
|
1607
|
+
resetWifiProtocolSession(sessionId: "", code: "sdk_closed")
|
|
1514
1608
|
if let bluetoothAvailabilityListenerId {
|
|
1515
1609
|
BluetoothAvailability.shared.removeStateListener(bluetoothAvailabilityListenerId)
|
|
1516
1610
|
self.bluetoothAvailabilityListenerId = nil
|
|
@@ -1605,90 +1699,6 @@ public final class MentraBluetoothSDK {
|
|
|
1605
1699
|
}
|
|
1606
1700
|
}
|
|
1607
1701
|
|
|
1608
|
-
private func startStreamKeepAliveMonitor(streamId: String, intervalSeconds requestedIntervalSeconds: Int) {
|
|
1609
|
-
let intervalSeconds = requestedIntervalSeconds > 0 ? requestedIntervalSeconds : Self.defaultStreamKeepAliveIntervalSeconds
|
|
1610
|
-
let tracker = ActiveStreamKeepAlive(streamId: streamId, intervalSeconds: intervalSeconds)
|
|
1611
|
-
activeStreamKeepAlive = tracker
|
|
1612
|
-
sendNextStreamKeepAlive(for: tracker)
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
private func stopStreamKeepAliveMonitor() {
|
|
1616
|
-
activeStreamKeepAlive?.task?.cancel()
|
|
1617
|
-
activeStreamKeepAlive = nil
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
private func sendNextStreamKeepAlive(for tracker: ActiveStreamKeepAlive) {
|
|
1621
|
-
guard activeStreamKeepAlive === tracker else { return }
|
|
1622
|
-
|
|
1623
|
-
if tracker.armed, tracker.pendingAckId != nil {
|
|
1624
|
-
tracker.missedAckCount += 1
|
|
1625
|
-
if tracker.missedAckCount >= 3 {
|
|
1626
|
-
activeStreamKeepAlive = nil
|
|
1627
|
-
tracker.task?.cancel()
|
|
1628
|
-
let event = StreamStatusEvent(
|
|
1629
|
-
status: .error(
|
|
1630
|
-
streamId: tracker.streamId,
|
|
1631
|
-
errorDetails: "Stream keep-alive timed out after \(tracker.missedAckCount) missed ACKs",
|
|
1632
|
-
timestamp: Int(Date().timeIntervalSince1970 * 1000),
|
|
1633
|
-
resolvedConfig: nil
|
|
1634
|
-
)
|
|
1635
|
-
)
|
|
1636
|
-
delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(event))
|
|
1637
|
-
stopStreamKeepAliveMonitor()
|
|
1638
|
-
DeviceManager.shared.stopStream()
|
|
1639
|
-
return
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
let ackId = "ack-\(Int(Date().timeIntervalSince1970 * 1000))"
|
|
1644
|
-
tracker.pendingAckId = ackId
|
|
1645
|
-
DeviceManager.shared.keepStreamAlive(
|
|
1646
|
-
StreamKeepAliveRequest(streamId: tracker.streamId, ackId: ackId).values
|
|
1647
|
-
)
|
|
1648
|
-
|
|
1649
|
-
tracker.task?.cancel()
|
|
1650
|
-
tracker.task = Task { @MainActor [weak self, weak tracker] in
|
|
1651
|
-
guard let tracker else { return }
|
|
1652
|
-
try? await Task.sleep(nanoseconds: UInt64(tracker.intervalSeconds) * 1_000_000_000)
|
|
1653
|
-
self?.sendNextStreamKeepAlive(for: tracker)
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
private func handleStreamKeepAliveAck(_ event: KeepAliveAckEvent) -> Bool {
|
|
1658
|
-
guard let tracker = activeStreamKeepAlive,
|
|
1659
|
-
event.streamId == tracker.streamId,
|
|
1660
|
-
event.ackId == tracker.pendingAckId
|
|
1661
|
-
else {
|
|
1662
|
-
return false
|
|
1663
|
-
}
|
|
1664
|
-
tracker.pendingAckId = nil
|
|
1665
|
-
tracker.missedAckCount = 0
|
|
1666
|
-
return true
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
private func handleStreamStatusForKeepAlive(_ status: StreamStatus) {
|
|
1670
|
-
guard let streamId = status.streamId,
|
|
1671
|
-
activeStreamKeepAlive?.streamId == streamId
|
|
1672
|
-
else {
|
|
1673
|
-
return
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
switch status.state {
|
|
1677
|
-
case .stopped, .stopping, .error, .reconnectFailed:
|
|
1678
|
-
stopStreamKeepAliveMonitor()
|
|
1679
|
-
default:
|
|
1680
|
-
// A non-terminal status means the stream is live or coming up and the glasses can
|
|
1681
|
-
// now ACK; arm the missed-ACK detector from here so a slow startup before the first
|
|
1682
|
-
// ACK can't trip a false keep-alive timeout. On the arming transition, drop any
|
|
1683
|
-
// pre-arm bookkeeping so a stale unacked id can't immediately count as a miss.
|
|
1684
|
-
if let tracker = activeStreamKeepAlive, !tracker.armed {
|
|
1685
|
-
tracker.armed = true
|
|
1686
|
-
tracker.pendingAckId = nil
|
|
1687
|
-
tracker.missedAckCount = 0
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
1702
|
private func handleStreamStatusForRequests(_ event: StreamStatusEvent) {
|
|
1693
1703
|
if let (streamId, start) = matchingStreamStart(for: event) {
|
|
1694
1704
|
// Preemption is only proven by statuses the start path emits after
|
|
@@ -1756,13 +1766,13 @@ public final class MentraBluetoothSDK {
|
|
|
1756
1766
|
}
|
|
1757
1767
|
}
|
|
1758
1768
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1769
|
+
/// The glasses process BLE commands FIFO and every start_stream begins by
|
|
1770
|
+
/// stopping whatever runs, so an id-carrying status proving start X's
|
|
1771
|
+
/// start path ran on the glasses also proves every lower-seq start has
|
|
1772
|
+
/// already been preempted. Their only verdict on current firmware is a
|
|
1773
|
+
/// streamId-less stopped, which the id-less heuristic deliberately
|
|
1774
|
+
/// ignores — without this they would run out the 30s timeout instead of
|
|
1775
|
+
/// failing fast.
|
|
1766
1776
|
private func rejectPreemptedStreamStarts(winnerSeq: Int) {
|
|
1767
1777
|
for (streamId, start) in pendingStreamStarts where start.seq < winnerSeq {
|
|
1768
1778
|
pendingStreamStarts.removeValue(forKey: streamId)
|
|
@@ -1776,11 +1786,11 @@ public final class MentraBluetoothSDK {
|
|
|
1776
1786
|
}
|
|
1777
1787
|
}
|
|
1778
1788
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1789
|
+
/// The glasses process BLE commands FIFO, so this stop's ack also settles
|
|
1790
|
+
/// every start sent before it: whatever those starts brought up (or would
|
|
1791
|
+
/// have brought up) has been stopped, and no further status will arrive
|
|
1792
|
+
/// for them. Without this they would run out the 30s start timeout.
|
|
1793
|
+
/// Starts sent after the stop keep waiting for their own statuses.
|
|
1784
1794
|
private func rejectStreamStartsSuperseded(byStopSeq stopSeq: Int) {
|
|
1785
1795
|
for (streamId, start) in pendingStreamStarts where start.seq < stopSeq {
|
|
1786
1796
|
pendingStreamStarts.removeValue(forKey: streamId)
|
|
@@ -1982,9 +1992,9 @@ public final class MentraBluetoothSDK {
|
|
|
1982
1992
|
request.pending.resolve(results)
|
|
1983
1993
|
}
|
|
1984
1994
|
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1995
|
+
/// scanId-echoing glasses: results for another scan are ignored instead of
|
|
1996
|
+
/// resolving the pending request, and matching chunks accumulate until the
|
|
1997
|
+
/// glasses flag the scan complete.
|
|
1988
1998
|
private func handleCorrelatedWifiScanChunk(
|
|
1989
1999
|
scanId: String,
|
|
1990
2000
|
results: [WifiScanResult],
|
|
@@ -2010,49 +2020,167 @@ public final class MentraBluetoothSDK {
|
|
|
2010
2020
|
}
|
|
2011
2021
|
|
|
2012
2022
|
private func handleWifiStatusForRequests(_ event: WifiStatusEvent) {
|
|
2013
|
-
|
|
2023
|
+
let connectRequest = pendingWifiStatus
|
|
2014
2024
|
// A wifi_status carrying the explicit error field is the glasses' failure
|
|
2015
2025
|
// verdict for the in-flight connect: reject now instead of running out the
|
|
2016
2026
|
// request timeout. Only the error field counts as failure — the glasses'
|
|
2017
2027
|
// connect sequence emits a debounced bare connected=false ~1-2s after
|
|
2018
2028
|
// credentials while association is still in progress, and rejecting on that
|
|
2019
2029
|
// would kill every connect attempt early.
|
|
2020
|
-
if
|
|
2021
|
-
if pendingWifiStatus ===
|
|
2030
|
+
if let connectRequest, let error = event.error {
|
|
2031
|
+
if pendingWifiStatus === connectRequest {
|
|
2022
2032
|
pendingWifiStatus = nil
|
|
2023
2033
|
}
|
|
2024
|
-
|
|
2034
|
+
connectRequest.pending.reject(
|
|
2025
2035
|
BluetoothSdkError(
|
|
2026
2036
|
code: error,
|
|
2027
|
-
message: "Glasses failed to join \"\(
|
|
2037
|
+
message: "Glasses failed to join \"\(connectRequest.ssid)\": \(error)"
|
|
2028
2038
|
)
|
|
2029
2039
|
)
|
|
2030
2040
|
return
|
|
2031
2041
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2042
|
+
if let connectRequest, wifiStatusMatchesConnect(event.status, ssid: connectRequest.ssid) {
|
|
2043
|
+
if pendingWifiStatus === connectRequest {
|
|
2044
|
+
pendingWifiStatus = nil
|
|
2045
|
+
}
|
|
2046
|
+
connectRequest.pending.resolve(event)
|
|
2047
|
+
return
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Forget never settles from link state: modern requests require a correlated result,
|
|
2051
|
+
// while legacy requests resolve as unverified at accepted dispatch.
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
private func handleWifiForgetResultForRequests(_ data: [String: Any]) {
|
|
2055
|
+
guard let request = pendingWifiForget,
|
|
2056
|
+
request.mode == .modern,
|
|
2057
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2058
|
+
case let .supported(version) = wifiSessionCapabilities.forgetResult,
|
|
2059
|
+
let result = parseWifiForgetResult(
|
|
2060
|
+
expectedRequestId: request.requestId,
|
|
2061
|
+
expectedSid: request.sid,
|
|
2062
|
+
expectedSsid: request.ssid,
|
|
2063
|
+
capabilityVersion: version,
|
|
2064
|
+
data: data
|
|
2065
|
+
)
|
|
2066
|
+
else { return }
|
|
2067
|
+
if pendingWifiForget === request, request.epoch == wifiSessionCapabilities.epoch {
|
|
2068
|
+
pendingWifiForget = nil
|
|
2069
|
+
}
|
|
2070
|
+
request.pending.resolve(result)
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
private func handleSavedWifiNetworksForRequests(_ data: [String: Any]) {
|
|
2074
|
+
guard let request = pendingSavedWifiNetworks,
|
|
2075
|
+
request.mode == .modern,
|
|
2076
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2077
|
+
case let .supported(version) = wifiSessionCapabilities.savedNetworks,
|
|
2078
|
+
let result = parseSavedWifiNetworks(
|
|
2079
|
+
expectedRequestId: request.requestId,
|
|
2080
|
+
expectedSid: request.sid,
|
|
2081
|
+
capabilityVersion: version,
|
|
2082
|
+
data: data
|
|
2083
|
+
)
|
|
2084
|
+
else { return }
|
|
2085
|
+
if pendingSavedWifiNetworks === request, request.epoch == wifiSessionCapabilities.epoch {
|
|
2086
|
+
pendingSavedWifiNetworks = nil
|
|
2087
|
+
}
|
|
2088
|
+
request.pending.resolve(result)
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
private func wifiStatusMatchesConnect(_ status: WifiStatus, ssid: String) -> Bool {
|
|
2092
|
+
if case let .connected(currentSsid, _) = status {
|
|
2093
|
+
return currentSsid == ssid
|
|
2094
|
+
}
|
|
2095
|
+
return false
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
private func dispatchWifiForgetIfReady(_ request: PendingWifiForgetRequest) {
|
|
2099
|
+
guard pendingWifiForget === request,
|
|
2100
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2101
|
+
request.mode != .discovering,
|
|
2102
|
+
!request.commandSent
|
|
2103
|
+
else { return }
|
|
2104
|
+
if request.mode == .unsupported {
|
|
2105
|
+
request.pending.reject(BluetoothSdkError(code: "wifi_protocol_unsupported", message: "Unsupported WiFi forget protocol version."))
|
|
2106
|
+
return
|
|
2107
|
+
}
|
|
2108
|
+
request.sid = wifiSessionCapabilities.sessionId
|
|
2109
|
+
request.commandSent = DeviceManager.shared.forgetWifiNetwork(
|
|
2110
|
+
request.ssid,
|
|
2111
|
+
requestId: request.mode == .modern ? request.requestId : nil,
|
|
2112
|
+
sid: request.mode == .modern ? request.sid : nil
|
|
2113
|
+
)
|
|
2114
|
+
if !request.commandSent {
|
|
2115
|
+
request.pending.reject(BluetoothSdkError(code: "dispatch_failed", message: "No active glasses transport accepted WiFi forget."))
|
|
2116
|
+
} else if request.mode == .legacy {
|
|
2117
|
+
pendingWifiForget = nil
|
|
2118
|
+
request.pending.resolve(legacyWifiForgetResult(ssid: request.ssid))
|
|
2035
2119
|
}
|
|
2036
|
-
request.pending.resolve(event)
|
|
2037
2120
|
}
|
|
2038
2121
|
|
|
2039
|
-
private func
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2122
|
+
private func dispatchSavedWifiNetworksIfReady(_ request: PendingSavedWifiNetworks) {
|
|
2123
|
+
guard pendingSavedWifiNetworks === request,
|
|
2124
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2125
|
+
request.mode == .modern,
|
|
2126
|
+
!request.commandSent
|
|
2127
|
+
else { return }
|
|
2128
|
+
request.sid = wifiSessionCapabilities.sessionId
|
|
2129
|
+
request.commandSent = DeviceManager.shared.requestSavedWifiNetworks(requestId: request.requestId, sid: request.sid)
|
|
2130
|
+
if !request.commandSent {
|
|
2131
|
+
request.pending.reject(BluetoothSdkError(code: "dispatch_failed", message: "No active glasses transport accepted saved WiFi request."))
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
private func applyWifiProtocolCapabilities(_ data: [String: Any]) {
|
|
2136
|
+
wifiSessionCapabilities.applyVersionInfo1(data)
|
|
2137
|
+
if let request = pendingWifiForget,
|
|
2138
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2139
|
+
request.mode == .discovering
|
|
2140
|
+
{
|
|
2141
|
+
request.mode = wifiSessionCapabilities.forgetMode()
|
|
2142
|
+
dispatchWifiForgetIfReady(request)
|
|
2143
|
+
}
|
|
2144
|
+
if let request = pendingSavedWifiNetworks,
|
|
2145
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2146
|
+
request.mode == .discovering
|
|
2147
|
+
{
|
|
2148
|
+
request.mode = wifiSessionCapabilities.savedNetworksMode()
|
|
2149
|
+
if request.mode == .legacy || request.mode == .unsupported {
|
|
2150
|
+
pendingSavedWifiNetworks = nil
|
|
2151
|
+
request.pending.resolve(
|
|
2152
|
+
SavedWifiNetworksResult(
|
|
2153
|
+
outcome: .unsupported,
|
|
2154
|
+
networks: [],
|
|
2155
|
+
error: "saved_wifi_networks_unsupported"
|
|
2156
|
+
)
|
|
2157
|
+
)
|
|
2158
|
+
} else {
|
|
2159
|
+
dispatchSavedWifiNetworksIfReady(request)
|
|
2052
2160
|
}
|
|
2053
2161
|
}
|
|
2054
2162
|
}
|
|
2055
2163
|
|
|
2164
|
+
private func resetWifiProtocolSession(sessionId: String, code: String) {
|
|
2165
|
+
let error = BluetoothSdkError(code: code, message: "The glasses WiFi protocol session changed.")
|
|
2166
|
+
let wifiStatus = pendingWifiStatus?.pending
|
|
2167
|
+
let wifiForget = pendingWifiForget?.pending
|
|
2168
|
+
let savedNetworks = pendingSavedWifiNetworks?.pending
|
|
2169
|
+
let wifiScan = pendingWifiScan?.pending
|
|
2170
|
+
let hotspot = pendingHotspotStatus?.pending
|
|
2171
|
+
wifiSessionCapabilities.reset(sessionId: sessionId)
|
|
2172
|
+
pendingWifiStatus = nil
|
|
2173
|
+
pendingWifiForget = nil
|
|
2174
|
+
pendingSavedWifiNetworks = nil
|
|
2175
|
+
pendingWifiScan = nil
|
|
2176
|
+
pendingHotspotStatus = nil
|
|
2177
|
+
wifiStatus?.reject(error)
|
|
2178
|
+
wifiForget?.reject(error)
|
|
2179
|
+
savedNetworks?.reject(error)
|
|
2180
|
+
wifiScan?.reject(error)
|
|
2181
|
+
hotspot?.reject(error)
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2056
2184
|
private func handleHotspotStatusForRequests(_ event: HotspotStatusEvent) {
|
|
2057
2185
|
guard let request = pendingHotspotStatus else { return }
|
|
2058
2186
|
guard hotspotStatusMatches(event.status, enabled: request.enabled) else { return }
|
|
@@ -2085,6 +2213,9 @@ public final class MentraBluetoothSDK {
|
|
|
2085
2213
|
private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
|
|
2086
2214
|
switch ObservableStore.normalizeCategory(category) {
|
|
2087
2215
|
case "glasses":
|
|
2216
|
+
if changes["connected"] as? Bool == false {
|
|
2217
|
+
resetWifiProtocolSession(sessionId: "", code: "wifi_session_disconnected")
|
|
2218
|
+
}
|
|
2088
2219
|
analytics.observeGlassesStatus(glassesStatus)
|
|
2089
2220
|
let nextState = state
|
|
2090
2221
|
delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
|
|
@@ -2129,7 +2260,8 @@ public final class MentraBluetoothSDK {
|
|
|
2129
2260
|
projectName: projectName
|
|
2130
2261
|
)
|
|
2131
2262
|
}
|
|
2132
|
-
|
|
2263
|
+
|
|
2264
|
+
private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
2133
2265
|
guard let results = rawSearchResults as? [[String: Any]] else { return }
|
|
2134
2266
|
for result in results {
|
|
2135
2267
|
guard let name = result["name"] as? String else { continue }
|
|
@@ -2251,6 +2383,23 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
|
2251
2383
|
let event = WifiStatusEvent(values: data)
|
|
2252
2384
|
handleWifiStatusForRequests(event)
|
|
2253
2385
|
delegate?.mentraBluetoothSDK(self, didReceive: .wifiStatus(event))
|
|
2386
|
+
case "wifi_forget_result":
|
|
2387
|
+
handleWifiForgetResultForRequests(data)
|
|
2388
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
|
|
2389
|
+
case "saved_wifi_networks":
|
|
2390
|
+
handleSavedWifiNetworksForRequests(data)
|
|
2391
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
|
|
2392
|
+
case "wifi_protocol_session_ready":
|
|
2393
|
+
resetWifiProtocolSession(
|
|
2394
|
+
sessionId: data["sid"] as? String ?? "",
|
|
2395
|
+
code: "wifi_session_restarted"
|
|
2396
|
+
)
|
|
2397
|
+
case "glasses_session_changed":
|
|
2398
|
+
resetWifiProtocolSession(
|
|
2399
|
+
sessionId: data["sid"] as? String ?? "",
|
|
2400
|
+
code: "wifi_session_changed"
|
|
2401
|
+
)
|
|
2402
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
|
|
2254
2403
|
case "wifi_scan_result":
|
|
2255
2404
|
let networks = (data["networks"] as? [[String: Any]])?.map(WifiScanResult.init(values:)) ?? []
|
|
2256
2405
|
let hasCompletionFlag = data.keys.contains("scanComplete") || data.keys.contains("scan_complete")
|
|
@@ -2300,16 +2449,16 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
|
2300
2449
|
let event = RgbLedControlResponseEvent(values: data)
|
|
2301
2450
|
handleRgbLedResponseForRequests(event)
|
|
2302
2451
|
delegate?.mentraBluetoothSDK(self, didReceive: .rgbLedControlResponse(event))
|
|
2452
|
+
case "stream_control_ready":
|
|
2453
|
+
streamSession.ready(sessionId: data["sid"] as? String, controlVersion: data["streamControlVersion"] as? Int)
|
|
2303
2454
|
case "stream_status":
|
|
2455
|
+
if data["revision"] != nil && !streamSession.accept(data) { return }
|
|
2304
2456
|
let event = StreamStatusEvent(values: data)
|
|
2305
2457
|
handleStreamStatusForRequests(event)
|
|
2306
|
-
handleStreamStatusForKeepAlive(event.status)
|
|
2307
2458
|
delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(event))
|
|
2308
2459
|
case "keep_alive_ack":
|
|
2309
2460
|
let event = KeepAliveAckEvent(values: data)
|
|
2310
|
-
|
|
2311
|
-
delegate?.mentraBluetoothSDK(self, didReceive: .keepAliveAck(event))
|
|
2312
|
-
}
|
|
2461
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .keepAliveAck(event))
|
|
2313
2462
|
case "ota_start_ack":
|
|
2314
2463
|
var values = data
|
|
2315
2464
|
values["type"] = "ota_start_ack"
|
|
@@ -2332,6 +2481,9 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
|
2332
2481
|
handleSettingsAckForRequests(event)
|
|
2333
2482
|
delegate?.mentraBluetoothSDK(self, didReceive: .settingsAck(event))
|
|
2334
2483
|
case "version_info":
|
|
2484
|
+
if let infoType = data["versionInfoType"] as? String, ["version_info_1", "version_info"].contains(infoType) {
|
|
2485
|
+
applyWifiProtocolCapabilities(data)
|
|
2486
|
+
}
|
|
2335
2487
|
let event = VersionInfoResult(values: data)
|
|
2336
2488
|
handleVersionInfoForRequest(data)
|
|
2337
2489
|
delegate?.mentraBluetoothSDK(self, didReceive: .versionInfo(event))
|