@mentra/bluetooth-sdk 3.2.0-dev.200 → 3.2.0-dev.203
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 -1
- 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 +285 -29
- 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 +312 -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/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/build/BluetoothSdk.types.d.ts +62 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +3 -2
- 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 -2
- 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 +365 -93
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/sgcs/MentraLive.swift +170 -35
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +28 -0
- package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +76 -1
- package/src/_private/BluetoothSdkModule.ts +4 -1
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +11 -0
|
@@ -8,9 +8,9 @@ private final class ActiveStreamKeepAlive {
|
|
|
8
8
|
var pendingAckId: String?
|
|
9
9
|
var missedAckCount = 0
|
|
10
10
|
var task: Task<Void, Never>?
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
14
|
var armed = false
|
|
15
15
|
|
|
16
16
|
init(streamId: String, intervalSeconds: Int) {
|
|
@@ -47,8 +47,8 @@ private final class PendingWifiScan {
|
|
|
47
47
|
let pending: PendingResponse<[WifiScanResult]>
|
|
48
48
|
let scanId: String
|
|
49
49
|
var latestResults: [WifiScanResult] = []
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
/// Chunks accumulated from scanId-echoing glasses, deduplicated by SSID;
|
|
51
|
+
/// resolved only when the glasses flag the scan complete.
|
|
52
52
|
var accumulated: [WifiScanResult] = []
|
|
53
53
|
|
|
54
54
|
init(pending: PendingResponse<[WifiScanResult]>, scanId: String) {
|
|
@@ -57,24 +57,71 @@ private final class PendingWifiScan {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
private enum WifiStatusOperation {
|
|
61
|
-
case connect
|
|
62
|
-
case forget
|
|
63
|
-
}
|
|
64
|
-
|
|
65
60
|
@MainActor
|
|
66
61
|
private final class PendingWifiStatusRequest {
|
|
67
|
-
let operation: WifiStatusOperation
|
|
68
62
|
let ssid: String
|
|
69
63
|
let pending: PendingResponse<WifiStatusEvent>
|
|
70
64
|
|
|
71
|
-
init(
|
|
72
|
-
|
|
65
|
+
init(
|
|
66
|
+
ssid: String,
|
|
67
|
+
pending: PendingResponse<WifiStatusEvent>
|
|
68
|
+
) {
|
|
73
69
|
self.ssid = ssid
|
|
74
70
|
self.pending = pending
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
73
|
|
|
74
|
+
@MainActor
|
|
75
|
+
private final class PendingWifiForgetRequest {
|
|
76
|
+
let ssid: String
|
|
77
|
+
let requestId: String
|
|
78
|
+
var sid: String
|
|
79
|
+
let epoch: UInt64
|
|
80
|
+
let pending: PendingResponse<WifiForgetResult>
|
|
81
|
+
var mode: WifiRequestMode
|
|
82
|
+
var commandSent = false
|
|
83
|
+
|
|
84
|
+
init(
|
|
85
|
+
ssid: String,
|
|
86
|
+
requestId: String,
|
|
87
|
+
sid: String,
|
|
88
|
+
epoch: UInt64,
|
|
89
|
+
pending: PendingResponse<WifiForgetResult>,
|
|
90
|
+
mode: WifiRequestMode
|
|
91
|
+
) {
|
|
92
|
+
self.ssid = ssid
|
|
93
|
+
self.requestId = requestId
|
|
94
|
+
self.sid = sid
|
|
95
|
+
self.epoch = epoch
|
|
96
|
+
self.pending = pending
|
|
97
|
+
self.mode = mode
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@MainActor
|
|
102
|
+
private final class PendingSavedWifiNetworks {
|
|
103
|
+
let requestId: String
|
|
104
|
+
var sid: String
|
|
105
|
+
let epoch: UInt64
|
|
106
|
+
let pending: PendingResponse<SavedWifiNetworksResult>
|
|
107
|
+
var mode: WifiRequestMode
|
|
108
|
+
var commandSent = false
|
|
109
|
+
|
|
110
|
+
init(
|
|
111
|
+
requestId: String,
|
|
112
|
+
sid: String,
|
|
113
|
+
epoch: UInt64,
|
|
114
|
+
pending: PendingResponse<SavedWifiNetworksResult>,
|
|
115
|
+
mode: WifiRequestMode
|
|
116
|
+
) {
|
|
117
|
+
self.requestId = requestId
|
|
118
|
+
self.sid = sid
|
|
119
|
+
self.epoch = epoch
|
|
120
|
+
self.pending = pending
|
|
121
|
+
self.mode = mode
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
78
125
|
@MainActor
|
|
79
126
|
private final class PendingHotspotStatusRequest {
|
|
80
127
|
let enabled: Bool
|
|
@@ -112,17 +159,17 @@ private final class PendingVersionInfoRequest {
|
|
|
112
159
|
}
|
|
113
160
|
}
|
|
114
161
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
162
|
+
/// seq records send order (assigned and handed to the BLE queue in a single
|
|
163
|
+
/// MainActor turn) so that an id-carrying status for a newer start can
|
|
164
|
+
/// identify which older in-flight starts it preempted.
|
|
118
165
|
@MainActor
|
|
119
166
|
private final class PendingStreamStart {
|
|
120
167
|
let seq: Int
|
|
121
168
|
let pending: PendingResponse<StreamStatusEvent>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
169
|
+
/// Set when an id-carrying error arrives: a fatal publisher error never
|
|
170
|
+
/// reaches the reconnect machinery and winds down with a streamId-less
|
|
171
|
+
/// stopped, so the stash both attributes that stopped to this start and
|
|
172
|
+
/// preserves the real error details for the rejection.
|
|
126
173
|
var lastError: StreamStatusEvent?
|
|
127
174
|
|
|
128
175
|
init(seq: Int, pending: PendingResponse<StreamStatusEvent>) {
|
|
@@ -132,7 +179,7 @@ private final class PendingStreamStart {
|
|
|
132
179
|
}
|
|
133
180
|
|
|
134
181
|
@MainActor
|
|
135
|
-
|
|
182
|
+
final class PendingResponse<T> {
|
|
136
183
|
private let operation: String
|
|
137
184
|
private var continuation: CheckedContinuation<T, Error>?
|
|
138
185
|
private var timeoutTask: Task<Void, Never>?
|
|
@@ -161,22 +208,39 @@ private final class PendingResponse<T> {
|
|
|
161
208
|
continuation = nil
|
|
162
209
|
}
|
|
163
210
|
|
|
164
|
-
func wait(timeoutMs: Int =
|
|
211
|
+
func wait(timeoutMs: Int? = 15000) async throws -> T {
|
|
212
|
+
if Task.isCancelled {
|
|
213
|
+
throw BluetoothSdkError(code: "request_cancelled", message: "\(operation) was cancelled.")
|
|
214
|
+
}
|
|
165
215
|
if let result {
|
|
166
216
|
return try result.get()
|
|
167
217
|
}
|
|
168
|
-
return try await
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
218
|
+
return try await withTaskCancellationHandler {
|
|
219
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
220
|
+
self.continuation = continuation
|
|
221
|
+
if let timeoutMs {
|
|
222
|
+
timeoutTask = Task { @MainActor [weak self] in
|
|
223
|
+
do {
|
|
224
|
+
try await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000)
|
|
225
|
+
} catch {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
self?.reject(
|
|
229
|
+
BluetoothSdkError(
|
|
230
|
+
code: "request_timeout",
|
|
231
|
+
message: "\(self?.operation ?? "Request") timed out waiting for glasses response."
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
}
|
|
175
235
|
}
|
|
176
|
-
|
|
236
|
+
}
|
|
237
|
+
} onCancel: {
|
|
238
|
+
Task { @MainActor [weak self] in
|
|
239
|
+
guard let self else { return }
|
|
240
|
+
reject(
|
|
177
241
|
BluetoothSdkError(
|
|
178
|
-
code: "
|
|
179
|
-
message: "\(
|
|
242
|
+
code: "request_cancelled",
|
|
243
|
+
message: "\(operation) was cancelled."
|
|
180
244
|
)
|
|
181
245
|
)
|
|
182
246
|
}
|
|
@@ -186,12 +250,12 @@ private final class PendingResponse<T> {
|
|
|
186
250
|
|
|
187
251
|
@MainActor
|
|
188
252
|
public final class MentraBluetoothSDK {
|
|
189
|
-
private static let wifiScanTimeoutMs =
|
|
253
|
+
private static let wifiScanTimeoutMs = 20000
|
|
190
254
|
// A photo response is terminal only after capture, encoding, transport, and upload.
|
|
191
255
|
// 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 =
|
|
256
|
+
private static let photoRequestTimeoutMs = 30000
|
|
257
|
+
private static let otaBesVersionWaitMs = 5000
|
|
258
|
+
private static let otaMtkVersionWaitMs = 2000
|
|
195
259
|
private static let otaVersionPollMs = 100
|
|
196
260
|
private static let defaultStreamKeepAliveIntervalSeconds = 5
|
|
197
261
|
|
|
@@ -227,10 +291,13 @@ public final class MentraBluetoothSDK {
|
|
|
227
291
|
private var pendingOtaQuery: PendingResponse<OtaQueryResult>?
|
|
228
292
|
private var pendingOtaStart: PendingResponse<OtaStartAckEvent>?
|
|
229
293
|
private var pendingWifiScan: PendingWifiScan?
|
|
294
|
+
private var pendingSavedWifiNetworks: PendingSavedWifiNetworks?
|
|
230
295
|
private var wifiScanTask: Task<[WifiScanResult], Error>?
|
|
231
296
|
private var pendingWifiStatus: PendingWifiStatusRequest?
|
|
297
|
+
private var pendingWifiForget: PendingWifiForgetRequest?
|
|
232
298
|
private var pendingHotspotStatus: PendingHotspotStatusRequest?
|
|
233
299
|
private var pendingVersionInfo: PendingVersionInfoRequest?
|
|
300
|
+
private let wifiSessionCapabilities = WifiSessionCapabilities()
|
|
234
301
|
private var configuredOtaVersionUrl: String?
|
|
235
302
|
|
|
236
303
|
public init(configuration: MentraBluetoothSDKConfiguration = .default) {
|
|
@@ -620,7 +687,8 @@ public final class MentraBluetoothSDK {
|
|
|
620
687
|
for key in ["button_photo_zsl_mfnr", "button_photo_mfnr", "button_photo_zsl", "button_photo_noise_reduction",
|
|
621
688
|
"button_photo_edge_enhancement", "button_photo_isp_digital_gain",
|
|
622
689
|
"button_photo_isp_analog_gain", "button_photo_ae_exposure_divisor",
|
|
623
|
-
"button_photo_iso_cap", "button_photo_compress", "button_photo_sound"]
|
|
690
|
+
"button_photo_iso_cap", "button_photo_compress", "button_photo_sound"]
|
|
691
|
+
{
|
|
624
692
|
DeviceStore.shared.remove(cat, key)
|
|
625
693
|
}
|
|
626
694
|
}
|
|
@@ -874,7 +942,8 @@ public final class MentraBluetoothSDK {
|
|
|
874
942
|
return try await pending.wait(timeoutMs: MentraBluetoothSDK.wifiScanTimeoutMs)
|
|
875
943
|
} catch {
|
|
876
944
|
if (error as? BluetoothSdkError)?.code == "request_timeout",
|
|
877
|
-
!request.latestResults.isEmpty
|
|
945
|
+
!request.latestResults.isEmpty
|
|
946
|
+
{
|
|
878
947
|
return request.latestResults
|
|
879
948
|
}
|
|
880
949
|
throw error
|
|
@@ -885,14 +954,14 @@ public final class MentraBluetoothSDK {
|
|
|
885
954
|
}
|
|
886
955
|
|
|
887
956
|
public func sendWifiCredentials(ssid: String, password: String) async throws -> WifiStatusEvent {
|
|
888
|
-
guard pendingWifiStatus == nil else {
|
|
957
|
+
guard pendingWifiStatus == nil, pendingWifiForget == nil else {
|
|
889
958
|
throw BluetoothSdkError(
|
|
890
959
|
code: "request_in_flight",
|
|
891
960
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
892
961
|
)
|
|
893
962
|
}
|
|
894
963
|
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi connect request")
|
|
895
|
-
pendingWifiStatus = PendingWifiStatusRequest(
|
|
964
|
+
pendingWifiStatus = PendingWifiStatusRequest(ssid: ssid, pending: pending)
|
|
896
965
|
DeviceManager.shared.sendWifiCredentials(ssid, password)
|
|
897
966
|
do {
|
|
898
967
|
let event = try await pending.wait()
|
|
@@ -908,25 +977,83 @@ public final class MentraBluetoothSDK {
|
|
|
908
977
|
}
|
|
909
978
|
}
|
|
910
979
|
|
|
911
|
-
public func forgetWifiNetwork(ssid: String) async throws ->
|
|
912
|
-
guard
|
|
980
|
+
public func forgetWifiNetwork(ssid: String) async throws -> WifiForgetResult {
|
|
981
|
+
guard wifiSsidIsValid(ssid) else {
|
|
982
|
+
throw BluetoothSdkError(code: "invalid_ssid", message: "WiFi SSID cannot be empty.")
|
|
983
|
+
}
|
|
984
|
+
guard pendingWifiStatus == nil, pendingWifiForget == nil else {
|
|
913
985
|
throw BluetoothSdkError(
|
|
914
986
|
code: "request_in_flight",
|
|
915
987
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
916
988
|
)
|
|
917
989
|
}
|
|
918
|
-
let pending = PendingResponse<
|
|
919
|
-
|
|
920
|
-
|
|
990
|
+
let pending = PendingResponse<WifiForgetResult>(operation: "WiFi forget request")
|
|
991
|
+
let requestId = "forget-\(UUID().uuidString)"
|
|
992
|
+
let request = PendingWifiForgetRequest(
|
|
993
|
+
ssid: ssid,
|
|
994
|
+
requestId: requestId,
|
|
995
|
+
sid: wifiSessionCapabilities.sessionId,
|
|
996
|
+
epoch: wifiSessionCapabilities.epoch,
|
|
997
|
+
pending: pending,
|
|
998
|
+
mode: wifiSessionCapabilities.forgetMode()
|
|
999
|
+
)
|
|
1000
|
+
pendingWifiForget = request
|
|
1001
|
+
dispatchWifiForgetIfReady(request)
|
|
921
1002
|
do {
|
|
922
1003
|
let event = try await pending.wait()
|
|
923
|
-
if
|
|
924
|
-
|
|
1004
|
+
if pendingWifiForget === request {
|
|
1005
|
+
pendingWifiForget = nil
|
|
925
1006
|
}
|
|
926
1007
|
return event
|
|
927
1008
|
} catch {
|
|
928
|
-
if
|
|
929
|
-
|
|
1009
|
+
if pendingWifiForget === request {
|
|
1010
|
+
pendingWifiForget = nil
|
|
1011
|
+
}
|
|
1012
|
+
if let sdkError = error as? BluetoothSdkError, sdkError.code == "request_timeout", request.mode == .discovering {
|
|
1013
|
+
throw BluetoothSdkError(code: wifiCapabilityNegotiationTimeoutCode, message: "WiFi capability negotiation timed out.")
|
|
1014
|
+
}
|
|
1015
|
+
throw error
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
public func getSavedWifiNetworks() async throws -> SavedWifiNetworksResult {
|
|
1020
|
+
guard pendingSavedWifiNetworks == nil else {
|
|
1021
|
+
throw BluetoothSdkError(
|
|
1022
|
+
code: "request_in_flight",
|
|
1023
|
+
message: "A saved WiFi networks request is already waiting for a glasses response."
|
|
1024
|
+
)
|
|
1025
|
+
}
|
|
1026
|
+
let requestId = "saved-\(UUID().uuidString)"
|
|
1027
|
+
let mode = wifiSessionCapabilities.savedNetworksMode()
|
|
1028
|
+
if mode == .legacy || mode == .unsupported {
|
|
1029
|
+
return SavedWifiNetworksResult(
|
|
1030
|
+
outcome: .unsupported,
|
|
1031
|
+
networks: [],
|
|
1032
|
+
error: "saved_wifi_networks_unsupported"
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
let pending = PendingResponse<SavedWifiNetworksResult>(operation: "Saved WiFi networks request")
|
|
1036
|
+
let request = PendingSavedWifiNetworks(
|
|
1037
|
+
requestId: requestId,
|
|
1038
|
+
sid: wifiSessionCapabilities.sessionId,
|
|
1039
|
+
epoch: wifiSessionCapabilities.epoch,
|
|
1040
|
+
pending: pending,
|
|
1041
|
+
mode: mode
|
|
1042
|
+
)
|
|
1043
|
+
pendingSavedWifiNetworks = request
|
|
1044
|
+
dispatchSavedWifiNetworksIfReady(request)
|
|
1045
|
+
do {
|
|
1046
|
+
let networks = try await pending.wait()
|
|
1047
|
+
if pendingSavedWifiNetworks === request {
|
|
1048
|
+
pendingSavedWifiNetworks = nil
|
|
1049
|
+
}
|
|
1050
|
+
return networks
|
|
1051
|
+
} catch {
|
|
1052
|
+
if pendingSavedWifiNetworks === request {
|
|
1053
|
+
pendingSavedWifiNetworks = nil
|
|
1054
|
+
}
|
|
1055
|
+
if let sdkError = error as? BluetoothSdkError, sdkError.code == "request_timeout", request.mode == .discovering {
|
|
1056
|
+
throw BluetoothSdkError(code: wifiCapabilityNegotiationTimeoutCode, message: "WiFi capability negotiation timed out.")
|
|
930
1057
|
}
|
|
931
1058
|
throw error
|
|
932
1059
|
}
|
|
@@ -1075,7 +1202,7 @@ public final class MentraBluetoothSDK {
|
|
|
1075
1202
|
stopStreamKeepAliveMonitor()
|
|
1076
1203
|
DeviceManager.shared.startStream(values)
|
|
1077
1204
|
do {
|
|
1078
|
-
let event = try await pending.wait(timeoutMs:
|
|
1205
|
+
let event = try await pending.wait(timeoutMs: 30000)
|
|
1079
1206
|
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1080
1207
|
if startSdkKeepAlive {
|
|
1081
1208
|
startStreamKeepAliveMonitor(
|
|
@@ -1134,7 +1261,7 @@ public final class MentraBluetoothSDK {
|
|
|
1134
1261
|
stopStreamKeepAliveMonitor()
|
|
1135
1262
|
DeviceManager.shared.stopStream()
|
|
1136
1263
|
do {
|
|
1137
|
-
let event = try await pending.wait(timeoutMs:
|
|
1264
|
+
let event = try await pending.wait(timeoutMs: 15000)
|
|
1138
1265
|
if pendingStreamStop?.pending === pending {
|
|
1139
1266
|
pendingStreamStop = nil
|
|
1140
1267
|
}
|
|
@@ -1206,7 +1333,7 @@ public final class MentraBluetoothSDK {
|
|
|
1206
1333
|
)
|
|
1207
1334
|
DeviceManager.shared.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1208
1335
|
do {
|
|
1209
|
-
let timeoutMs = waitForUpload ? videoUploadStopTimeoutMs :
|
|
1336
|
+
let timeoutMs = waitForUpload ? videoUploadStopTimeoutMs : 15000
|
|
1210
1337
|
let event = try await pending.wait(timeoutMs: timeoutMs)
|
|
1211
1338
|
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
1212
1339
|
return event
|
|
@@ -1397,7 +1524,9 @@ public final class MentraBluetoothSDK {
|
|
|
1397
1524
|
try await startOtaCommand(otaVersionUrl: otaVersionUrl)
|
|
1398
1525
|
}
|
|
1399
1526
|
|
|
1400
|
-
func sendOtaQueryStatus() async throws -> OtaQueryResult {
|
|
1527
|
+
func sendOtaQueryStatus() async throws -> OtaQueryResult {
|
|
1528
|
+
try await queryOtaStatus()
|
|
1529
|
+
}
|
|
1401
1530
|
|
|
1402
1531
|
func startAr99OtaFromFile(_ path: String) throws -> Bool {
|
|
1403
1532
|
try requireGlassesConnected(operation: "start AR99 OTA")
|
|
@@ -1461,7 +1590,7 @@ public final class MentraBluetoothSDK {
|
|
|
1461
1590
|
timeoutMs: Int,
|
|
1462
1591
|
isReady: (GlassesStatus) -> Bool
|
|
1463
1592
|
) async -> GlassesStatus {
|
|
1464
|
-
let deadline = Date().addingTimeInterval(Double(timeoutMs) /
|
|
1593
|
+
let deadline = Date().addingTimeInterval(Double(timeoutMs) / 1000)
|
|
1465
1594
|
var status = initialStatus
|
|
1466
1595
|
while Date() < deadline {
|
|
1467
1596
|
status = glassesStatus
|
|
@@ -1469,7 +1598,7 @@ public final class MentraBluetoothSDK {
|
|
|
1469
1598
|
return status
|
|
1470
1599
|
}
|
|
1471
1600
|
|
|
1472
|
-
let remainingMs = max(0, Int(deadline.timeIntervalSinceNow *
|
|
1601
|
+
let remainingMs = max(0, Int(deadline.timeIntervalSinceNow * 1000))
|
|
1473
1602
|
let sleepMs = min(Self.otaVersionPollMs, remainingMs)
|
|
1474
1603
|
if sleepMs <= 0 {
|
|
1475
1604
|
break
|
|
@@ -1511,6 +1640,7 @@ public final class MentraBluetoothSDK {
|
|
|
1511
1640
|
|
|
1512
1641
|
public func invalidate() {
|
|
1513
1642
|
stopStreamKeepAliveMonitor()
|
|
1643
|
+
resetWifiProtocolSession(sessionId: "", code: "sdk_closed")
|
|
1514
1644
|
if let bluetoothAvailabilityListenerId {
|
|
1515
1645
|
BluetoothAvailability.shared.removeStateListener(bluetoothAvailabilityListenerId)
|
|
1516
1646
|
self.bluetoothAvailabilityListenerId = nil
|
|
@@ -1756,13 +1886,13 @@ public final class MentraBluetoothSDK {
|
|
|
1756
1886
|
}
|
|
1757
1887
|
}
|
|
1758
1888
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1889
|
+
/// The glasses process BLE commands FIFO and every start_stream begins by
|
|
1890
|
+
/// stopping whatever runs, so an id-carrying status proving start X's
|
|
1891
|
+
/// start path ran on the glasses also proves every lower-seq start has
|
|
1892
|
+
/// already been preempted. Their only verdict on current firmware is a
|
|
1893
|
+
/// streamId-less stopped, which the id-less heuristic deliberately
|
|
1894
|
+
/// ignores — without this they would run out the 30s timeout instead of
|
|
1895
|
+
/// failing fast.
|
|
1766
1896
|
private func rejectPreemptedStreamStarts(winnerSeq: Int) {
|
|
1767
1897
|
for (streamId, start) in pendingStreamStarts where start.seq < winnerSeq {
|
|
1768
1898
|
pendingStreamStarts.removeValue(forKey: streamId)
|
|
@@ -1776,11 +1906,11 @@ public final class MentraBluetoothSDK {
|
|
|
1776
1906
|
}
|
|
1777
1907
|
}
|
|
1778
1908
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1909
|
+
/// The glasses process BLE commands FIFO, so this stop's ack also settles
|
|
1910
|
+
/// every start sent before it: whatever those starts brought up (or would
|
|
1911
|
+
/// have brought up) has been stopped, and no further status will arrive
|
|
1912
|
+
/// for them. Without this they would run out the 30s start timeout.
|
|
1913
|
+
/// Starts sent after the stop keep waiting for their own statuses.
|
|
1784
1914
|
private func rejectStreamStartsSuperseded(byStopSeq stopSeq: Int) {
|
|
1785
1915
|
for (streamId, start) in pendingStreamStarts where start.seq < stopSeq {
|
|
1786
1916
|
pendingStreamStarts.removeValue(forKey: streamId)
|
|
@@ -1982,9 +2112,9 @@ public final class MentraBluetoothSDK {
|
|
|
1982
2112
|
request.pending.resolve(results)
|
|
1983
2113
|
}
|
|
1984
2114
|
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2115
|
+
/// scanId-echoing glasses: results for another scan are ignored instead of
|
|
2116
|
+
/// resolving the pending request, and matching chunks accumulate until the
|
|
2117
|
+
/// glasses flag the scan complete.
|
|
1988
2118
|
private func handleCorrelatedWifiScanChunk(
|
|
1989
2119
|
scanId: String,
|
|
1990
2120
|
results: [WifiScanResult],
|
|
@@ -2010,49 +2140,167 @@ public final class MentraBluetoothSDK {
|
|
|
2010
2140
|
}
|
|
2011
2141
|
|
|
2012
2142
|
private func handleWifiStatusForRequests(_ event: WifiStatusEvent) {
|
|
2013
|
-
|
|
2143
|
+
let connectRequest = pendingWifiStatus
|
|
2014
2144
|
// A wifi_status carrying the explicit error field is the glasses' failure
|
|
2015
2145
|
// verdict for the in-flight connect: reject now instead of running out the
|
|
2016
2146
|
// request timeout. Only the error field counts as failure — the glasses'
|
|
2017
2147
|
// connect sequence emits a debounced bare connected=false ~1-2s after
|
|
2018
2148
|
// credentials while association is still in progress, and rejecting on that
|
|
2019
2149
|
// would kill every connect attempt early.
|
|
2020
|
-
if
|
|
2021
|
-
if pendingWifiStatus ===
|
|
2150
|
+
if let connectRequest, let error = event.error {
|
|
2151
|
+
if pendingWifiStatus === connectRequest {
|
|
2022
2152
|
pendingWifiStatus = nil
|
|
2023
2153
|
}
|
|
2024
|
-
|
|
2154
|
+
connectRequest.pending.reject(
|
|
2025
2155
|
BluetoothSdkError(
|
|
2026
2156
|
code: error,
|
|
2027
|
-
message: "Glasses failed to join \"\(
|
|
2157
|
+
message: "Glasses failed to join \"\(connectRequest.ssid)\": \(error)"
|
|
2028
2158
|
)
|
|
2029
2159
|
)
|
|
2030
2160
|
return
|
|
2031
2161
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2162
|
+
if let connectRequest, wifiStatusMatchesConnect(event.status, ssid: connectRequest.ssid) {
|
|
2163
|
+
if pendingWifiStatus === connectRequest {
|
|
2164
|
+
pendingWifiStatus = nil
|
|
2165
|
+
}
|
|
2166
|
+
connectRequest.pending.resolve(event)
|
|
2167
|
+
return
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// Forget never settles from link state: modern requests require a correlated result,
|
|
2171
|
+
// while legacy requests resolve as unverified at accepted dispatch.
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
private func handleWifiForgetResultForRequests(_ data: [String: Any]) {
|
|
2175
|
+
guard let request = pendingWifiForget,
|
|
2176
|
+
request.mode == .modern,
|
|
2177
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2178
|
+
case let .supported(version) = wifiSessionCapabilities.forgetResult,
|
|
2179
|
+
let result = parseWifiForgetResult(
|
|
2180
|
+
expectedRequestId: request.requestId,
|
|
2181
|
+
expectedSid: request.sid,
|
|
2182
|
+
expectedSsid: request.ssid,
|
|
2183
|
+
capabilityVersion: version,
|
|
2184
|
+
data: data
|
|
2185
|
+
)
|
|
2186
|
+
else { return }
|
|
2187
|
+
if pendingWifiForget === request, request.epoch == wifiSessionCapabilities.epoch {
|
|
2188
|
+
pendingWifiForget = nil
|
|
2189
|
+
}
|
|
2190
|
+
request.pending.resolve(result)
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
private func handleSavedWifiNetworksForRequests(_ data: [String: Any]) {
|
|
2194
|
+
guard let request = pendingSavedWifiNetworks,
|
|
2195
|
+
request.mode == .modern,
|
|
2196
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2197
|
+
case let .supported(version) = wifiSessionCapabilities.savedNetworks,
|
|
2198
|
+
let result = parseSavedWifiNetworks(
|
|
2199
|
+
expectedRequestId: request.requestId,
|
|
2200
|
+
expectedSid: request.sid,
|
|
2201
|
+
capabilityVersion: version,
|
|
2202
|
+
data: data
|
|
2203
|
+
)
|
|
2204
|
+
else { return }
|
|
2205
|
+
if pendingSavedWifiNetworks === request, request.epoch == wifiSessionCapabilities.epoch {
|
|
2206
|
+
pendingSavedWifiNetworks = nil
|
|
2207
|
+
}
|
|
2208
|
+
request.pending.resolve(result)
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
private func wifiStatusMatchesConnect(_ status: WifiStatus, ssid: String) -> Bool {
|
|
2212
|
+
if case let .connected(currentSsid, _) = status {
|
|
2213
|
+
return currentSsid == ssid
|
|
2214
|
+
}
|
|
2215
|
+
return false
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
private func dispatchWifiForgetIfReady(_ request: PendingWifiForgetRequest) {
|
|
2219
|
+
guard pendingWifiForget === request,
|
|
2220
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2221
|
+
request.mode != .discovering,
|
|
2222
|
+
!request.commandSent
|
|
2223
|
+
else { return }
|
|
2224
|
+
if request.mode == .unsupported {
|
|
2225
|
+
request.pending.reject(BluetoothSdkError(code: "wifi_protocol_unsupported", message: "Unsupported WiFi forget protocol version."))
|
|
2226
|
+
return
|
|
2227
|
+
}
|
|
2228
|
+
request.sid = wifiSessionCapabilities.sessionId
|
|
2229
|
+
request.commandSent = DeviceManager.shared.forgetWifiNetwork(
|
|
2230
|
+
request.ssid,
|
|
2231
|
+
requestId: request.mode == .modern ? request.requestId : nil,
|
|
2232
|
+
sid: request.mode == .modern ? request.sid : nil
|
|
2233
|
+
)
|
|
2234
|
+
if !request.commandSent {
|
|
2235
|
+
request.pending.reject(BluetoothSdkError(code: "dispatch_failed", message: "No active glasses transport accepted WiFi forget."))
|
|
2236
|
+
} else if request.mode == .legacy {
|
|
2237
|
+
pendingWifiForget = nil
|
|
2238
|
+
request.pending.resolve(legacyWifiForgetResult(ssid: request.ssid))
|
|
2035
2239
|
}
|
|
2036
|
-
request.pending.resolve(event)
|
|
2037
2240
|
}
|
|
2038
2241
|
|
|
2039
|
-
private func
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2242
|
+
private func dispatchSavedWifiNetworksIfReady(_ request: PendingSavedWifiNetworks) {
|
|
2243
|
+
guard pendingSavedWifiNetworks === request,
|
|
2244
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2245
|
+
request.mode == .modern,
|
|
2246
|
+
!request.commandSent
|
|
2247
|
+
else { return }
|
|
2248
|
+
request.sid = wifiSessionCapabilities.sessionId
|
|
2249
|
+
request.commandSent = DeviceManager.shared.requestSavedWifiNetworks(requestId: request.requestId, sid: request.sid)
|
|
2250
|
+
if !request.commandSent {
|
|
2251
|
+
request.pending.reject(BluetoothSdkError(code: "dispatch_failed", message: "No active glasses transport accepted saved WiFi request."))
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
private func applyWifiProtocolCapabilities(_ data: [String: Any]) {
|
|
2256
|
+
wifiSessionCapabilities.applyVersionInfo1(data)
|
|
2257
|
+
if let request = pendingWifiForget,
|
|
2258
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2259
|
+
request.mode == .discovering
|
|
2260
|
+
{
|
|
2261
|
+
request.mode = wifiSessionCapabilities.forgetMode()
|
|
2262
|
+
dispatchWifiForgetIfReady(request)
|
|
2263
|
+
}
|
|
2264
|
+
if let request = pendingSavedWifiNetworks,
|
|
2265
|
+
request.epoch == wifiSessionCapabilities.epoch,
|
|
2266
|
+
request.mode == .discovering
|
|
2267
|
+
{
|
|
2268
|
+
request.mode = wifiSessionCapabilities.savedNetworksMode()
|
|
2269
|
+
if request.mode == .legacy || request.mode == .unsupported {
|
|
2270
|
+
pendingSavedWifiNetworks = nil
|
|
2271
|
+
request.pending.resolve(
|
|
2272
|
+
SavedWifiNetworksResult(
|
|
2273
|
+
outcome: .unsupported,
|
|
2274
|
+
networks: [],
|
|
2275
|
+
error: "saved_wifi_networks_unsupported"
|
|
2276
|
+
)
|
|
2277
|
+
)
|
|
2278
|
+
} else {
|
|
2279
|
+
dispatchSavedWifiNetworksIfReady(request)
|
|
2052
2280
|
}
|
|
2053
2281
|
}
|
|
2054
2282
|
}
|
|
2055
2283
|
|
|
2284
|
+
private func resetWifiProtocolSession(sessionId: String, code: String) {
|
|
2285
|
+
let error = BluetoothSdkError(code: code, message: "The glasses WiFi protocol session changed.")
|
|
2286
|
+
let wifiStatus = pendingWifiStatus?.pending
|
|
2287
|
+
let wifiForget = pendingWifiForget?.pending
|
|
2288
|
+
let savedNetworks = pendingSavedWifiNetworks?.pending
|
|
2289
|
+
let wifiScan = pendingWifiScan?.pending
|
|
2290
|
+
let hotspot = pendingHotspotStatus?.pending
|
|
2291
|
+
wifiSessionCapabilities.reset(sessionId: sessionId)
|
|
2292
|
+
pendingWifiStatus = nil
|
|
2293
|
+
pendingWifiForget = nil
|
|
2294
|
+
pendingSavedWifiNetworks = nil
|
|
2295
|
+
pendingWifiScan = nil
|
|
2296
|
+
pendingHotspotStatus = nil
|
|
2297
|
+
wifiStatus?.reject(error)
|
|
2298
|
+
wifiForget?.reject(error)
|
|
2299
|
+
savedNetworks?.reject(error)
|
|
2300
|
+
wifiScan?.reject(error)
|
|
2301
|
+
hotspot?.reject(error)
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2056
2304
|
private func handleHotspotStatusForRequests(_ event: HotspotStatusEvent) {
|
|
2057
2305
|
guard let request = pendingHotspotStatus else { return }
|
|
2058
2306
|
guard hotspotStatusMatches(event.status, enabled: request.enabled) else { return }
|
|
@@ -2085,6 +2333,9 @@ public final class MentraBluetoothSDK {
|
|
|
2085
2333
|
private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
|
|
2086
2334
|
switch ObservableStore.normalizeCategory(category) {
|
|
2087
2335
|
case "glasses":
|
|
2336
|
+
if changes["connected"] as? Bool == false {
|
|
2337
|
+
resetWifiProtocolSession(sessionId: "", code: "wifi_session_disconnected")
|
|
2338
|
+
}
|
|
2088
2339
|
analytics.observeGlassesStatus(glassesStatus)
|
|
2089
2340
|
let nextState = state
|
|
2090
2341
|
delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
|
|
@@ -2129,7 +2380,8 @@ public final class MentraBluetoothSDK {
|
|
|
2129
2380
|
projectName: projectName
|
|
2130
2381
|
)
|
|
2131
2382
|
}
|
|
2132
|
-
|
|
2383
|
+
|
|
2384
|
+
private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
2133
2385
|
guard let results = rawSearchResults as? [[String: Any]] else { return }
|
|
2134
2386
|
for result in results {
|
|
2135
2387
|
guard let name = result["name"] as? String else { continue }
|
|
@@ -2251,6 +2503,23 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
|
2251
2503
|
let event = WifiStatusEvent(values: data)
|
|
2252
2504
|
handleWifiStatusForRequests(event)
|
|
2253
2505
|
delegate?.mentraBluetoothSDK(self, didReceive: .wifiStatus(event))
|
|
2506
|
+
case "wifi_forget_result":
|
|
2507
|
+
handleWifiForgetResultForRequests(data)
|
|
2508
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
|
|
2509
|
+
case "saved_wifi_networks":
|
|
2510
|
+
handleSavedWifiNetworksForRequests(data)
|
|
2511
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
|
|
2512
|
+
case "wifi_protocol_session_ready":
|
|
2513
|
+
resetWifiProtocolSession(
|
|
2514
|
+
sessionId: data["sid"] as? String ?? "",
|
|
2515
|
+
code: "wifi_session_restarted"
|
|
2516
|
+
)
|
|
2517
|
+
case "glasses_session_changed":
|
|
2518
|
+
resetWifiProtocolSession(
|
|
2519
|
+
sessionId: data["sid"] as? String ?? "",
|
|
2520
|
+
code: "wifi_session_changed"
|
|
2521
|
+
)
|
|
2522
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
|
|
2254
2523
|
case "wifi_scan_result":
|
|
2255
2524
|
let networks = (data["networks"] as? [[String: Any]])?.map(WifiScanResult.init(values:)) ?? []
|
|
2256
2525
|
let hasCompletionFlag = data.keys.contains("scanComplete") || data.keys.contains("scan_complete")
|
|
@@ -2332,6 +2601,9 @@ private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
|
2332
2601
|
handleSettingsAckForRequests(event)
|
|
2333
2602
|
delegate?.mentraBluetoothSDK(self, didReceive: .settingsAck(event))
|
|
2334
2603
|
case "version_info":
|
|
2604
|
+
if let infoType = data["versionInfoType"] as? String, ["version_info_1", "version_info"].contains(infoType) {
|
|
2605
|
+
applyWifiProtocolCapabilities(data)
|
|
2606
|
+
}
|
|
2335
2607
|
let event = VersionInfoResult(values: data)
|
|
2336
2608
|
handleVersionInfoForRequest(data)
|
|
2337
2609
|
delegate?.mentraBluetoothSDK(self, didReceive: .versionInfo(event))
|