@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
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import CoreFoundation
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
private func isWifiProtocolV1(_ raw: Any?) -> Bool {
|
|
5
|
+
guard let number = raw as? NSNumber,
|
|
6
|
+
CFGetTypeID(number) != CFBooleanGetTypeID()
|
|
7
|
+
else { return false }
|
|
8
|
+
return number.doubleValue == 1
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// Validate raw fields before convenience conversions erase malformed or partial tuples.
|
|
12
|
+
func wifiResponseEnvelopeIsValid(_ values: [String: Any], allowLegacy: Bool) -> Bool {
|
|
13
|
+
for key in ["connected", "dispatched"] where values[key] != nil {
|
|
14
|
+
guard let number = values[key] as? NSNumber, CFGetTypeID(number) == CFBooleanGetTypeID()
|
|
15
|
+
else { return false }
|
|
16
|
+
}
|
|
17
|
+
let hasTuple = ["protocol_version", "requestId", "sid"].contains { values[$0] != nil }
|
|
18
|
+
if !hasTuple { return allowLegacy && values["outcome"] == nil }
|
|
19
|
+
guard let requestId = values["requestId"] as? String,
|
|
20
|
+
let sid = values["sid"] as? String
|
|
21
|
+
else { return false }
|
|
22
|
+
return isWifiProtocolV1(values["protocol_version"]) && wifiSsidIsValid(requestId) && wifiSsidIsValid(sid)
|
|
23
|
+
&& values["dispatched"] == nil
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func wifiSsidIsValid(_ ssid: String) -> Bool {
|
|
27
|
+
!ssid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let wifiCapabilityNegotiationTimeoutCode = "capability_negotiation_timeout"
|
|
31
|
+
|
|
32
|
+
enum WifiProtocolCapability: Equatable {
|
|
33
|
+
case unknown
|
|
34
|
+
case supported(version: Int)
|
|
35
|
+
case unsupported
|
|
36
|
+
case legacy
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
enum WifiRequestMode: Equatable {
|
|
40
|
+
case discovering
|
|
41
|
+
case modern
|
|
42
|
+
case legacy
|
|
43
|
+
case unsupported
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
final class WifiSessionCapabilities {
|
|
47
|
+
private(set) var sessionId = ""
|
|
48
|
+
private(set) var epoch: UInt64 = 0
|
|
49
|
+
private(set) var forgetResult: WifiProtocolCapability = .unknown
|
|
50
|
+
private(set) var savedNetworks: WifiProtocolCapability = .unknown
|
|
51
|
+
|
|
52
|
+
func reset(sessionId: String = "") {
|
|
53
|
+
epoch &+= 1
|
|
54
|
+
self.sessionId = sessionId
|
|
55
|
+
forgetResult = .unknown
|
|
56
|
+
savedNetworks = .unknown
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func applyVersionInfo1(_ values: [String: Any]) {
|
|
60
|
+
if let sid = values["sid"] as? String, !sid.isEmpty {
|
|
61
|
+
sessionId = sid
|
|
62
|
+
}
|
|
63
|
+
forgetResult = capability(values["wifiForgetResultVersion"])
|
|
64
|
+
savedNetworks = capability(values["savedWifiNetworksVersion"])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func forgetMode() -> WifiRequestMode {
|
|
68
|
+
Self.requestMode(forgetResult)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func savedNetworksMode() -> WifiRequestMode {
|
|
72
|
+
Self.requestMode(savedNetworks)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func capability(_ raw: Any?) -> WifiProtocolCapability {
|
|
76
|
+
guard let raw else { return .legacy }
|
|
77
|
+
return isWifiProtocolV1(raw) && !sessionId.isEmpty ? .supported(version: 1) : .unsupported
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static func requestMode(_ capability: WifiProtocolCapability) -> WifiRequestMode {
|
|
81
|
+
switch capability {
|
|
82
|
+
case .unknown: .discovering
|
|
83
|
+
case .supported: .modern
|
|
84
|
+
case .legacy: .legacy
|
|
85
|
+
case .unsupported: .unsupported
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public enum WifiForgetOutcome: String {
|
|
91
|
+
case confirmed
|
|
92
|
+
case dispatched
|
|
93
|
+
case notFound = "not_found"
|
|
94
|
+
case unsupported
|
|
95
|
+
case failed
|
|
96
|
+
case legacyUnverified = "legacy_unverified"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func normalizeWifiForgetResultEvent(
|
|
100
|
+
requestId: String,
|
|
101
|
+
sid: String,
|
|
102
|
+
ssid: String,
|
|
103
|
+
protocolVersion: Int,
|
|
104
|
+
outcome: String,
|
|
105
|
+
legacyDispatched: Bool?,
|
|
106
|
+
connected: Bool?,
|
|
107
|
+
currentSsid: String,
|
|
108
|
+
localIp: String,
|
|
109
|
+
error: String?
|
|
110
|
+
) -> [String: Any]? {
|
|
111
|
+
guard wifiSsidIsValid(ssid) else { return nil }
|
|
112
|
+
if !requestId.isEmpty, !sid.isEmpty,
|
|
113
|
+
protocolVersion == 1,
|
|
114
|
+
let modernOutcome = WifiForgetOutcome(rawValue: outcome),
|
|
115
|
+
modernOutcome != .legacyUnverified
|
|
116
|
+
{
|
|
117
|
+
var body: [String: Any] = [
|
|
118
|
+
"mode": "modern",
|
|
119
|
+
"requestId": requestId,
|
|
120
|
+
"sid": sid,
|
|
121
|
+
"ssid": ssid,
|
|
122
|
+
"protocolVersion": protocolVersion,
|
|
123
|
+
"outcome": modernOutcome.rawValue,
|
|
124
|
+
]
|
|
125
|
+
if let connected { body["connected"] = connected }
|
|
126
|
+
if !currentSsid.isEmpty { body["currentSsid"] = currentSsid }
|
|
127
|
+
if !localIp.isEmpty { body["localIp"] = localIp }
|
|
128
|
+
if let error { body["error"] = error }
|
|
129
|
+
return body
|
|
130
|
+
}
|
|
131
|
+
if requestId.isEmpty, sid.isEmpty, protocolVersion == 0, outcome.isEmpty, let legacyDispatched {
|
|
132
|
+
var body: [String: Any] = [
|
|
133
|
+
"mode": "legacy",
|
|
134
|
+
"ssid": ssid,
|
|
135
|
+
"dispatched": legacyDispatched,
|
|
136
|
+
]
|
|
137
|
+
if let connected { body["connected"] = connected }
|
|
138
|
+
if !currentSsid.isEmpty { body["currentSsid"] = currentSsid }
|
|
139
|
+
if !localIp.isEmpty { body["localIp"] = localIp }
|
|
140
|
+
if let error { body["error"] = error }
|
|
141
|
+
return body
|
|
142
|
+
}
|
|
143
|
+
return nil
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public struct WifiForgetResult {
|
|
147
|
+
public let ssid: String
|
|
148
|
+
public let outcome: WifiForgetOutcome
|
|
149
|
+
public let connected: Bool?
|
|
150
|
+
public let currentSsid: String?
|
|
151
|
+
public let localIp: String?
|
|
152
|
+
public let error: String?
|
|
153
|
+
|
|
154
|
+
public var values: [String: Any] {
|
|
155
|
+
var result: [String: Any] = [
|
|
156
|
+
"ssid": ssid,
|
|
157
|
+
"outcome": outcome.rawValue,
|
|
158
|
+
]
|
|
159
|
+
if let connected { result["connected"] = connected }
|
|
160
|
+
if let currentSsid { result["currentSsid"] = currentSsid }
|
|
161
|
+
if let localIp { result["localIp"] = localIp }
|
|
162
|
+
if let error { result["error"] = error }
|
|
163
|
+
return result
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public enum SavedWifiNetworksOutcome: String {
|
|
168
|
+
case confirmed
|
|
169
|
+
case unsupported
|
|
170
|
+
case failed
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public struct SavedWifiNetworksResult {
|
|
174
|
+
public let outcome: SavedWifiNetworksOutcome
|
|
175
|
+
public let networks: [String]
|
|
176
|
+
public let error: String?
|
|
177
|
+
|
|
178
|
+
public var values: [String: Any] {
|
|
179
|
+
var result: [String: Any] = [
|
|
180
|
+
"outcome": outcome.rawValue,
|
|
181
|
+
"networks": networks,
|
|
182
|
+
]
|
|
183
|
+
if let error { result["error"] = error }
|
|
184
|
+
return result
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func parseWifiForgetResult(
|
|
189
|
+
expectedRequestId: String,
|
|
190
|
+
expectedSid: String,
|
|
191
|
+
expectedSsid: String,
|
|
192
|
+
capabilityVersion: Int,
|
|
193
|
+
data: [String: Any]
|
|
194
|
+
) -> WifiForgetResult? {
|
|
195
|
+
guard capabilityVersion == 1, !expectedRequestId.isEmpty, !expectedSid.isEmpty,
|
|
196
|
+
data["requestId"] as? String == expectedRequestId,
|
|
197
|
+
data["sid"] as? String == expectedSid,
|
|
198
|
+
data["ssid"] as? String == expectedSsid,
|
|
199
|
+
isWifiProtocolV1(data["protocolVersion"]),
|
|
200
|
+
let rawOutcome = data["outcome"] as? String,
|
|
201
|
+
let outcome = WifiForgetOutcome(rawValue: rawOutcome),
|
|
202
|
+
outcome != .legacyUnverified
|
|
203
|
+
else { return nil }
|
|
204
|
+
return WifiForgetResult(
|
|
205
|
+
ssid: expectedSsid,
|
|
206
|
+
outcome: outcome,
|
|
207
|
+
connected: data["connected"] as? Bool,
|
|
208
|
+
currentSsid: data["currentSsid"] as? String,
|
|
209
|
+
localIp: data["localIp"] as? String,
|
|
210
|
+
error: (data["error"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func parseSavedWifiNetworks(
|
|
215
|
+
expectedRequestId: String,
|
|
216
|
+
expectedSid: String,
|
|
217
|
+
capabilityVersion: Int,
|
|
218
|
+
data: [String: Any]
|
|
219
|
+
) -> SavedWifiNetworksResult? {
|
|
220
|
+
guard capabilityVersion == 1, !expectedRequestId.isEmpty, !expectedSid.isEmpty,
|
|
221
|
+
data["requestId"] as? String == expectedRequestId,
|
|
222
|
+
data["sid"] as? String == expectedSid,
|
|
223
|
+
isWifiProtocolV1(data["protocolVersion"]),
|
|
224
|
+
let rawOutcome = data["outcome"] as? String,
|
|
225
|
+
let outcome = SavedWifiNetworksOutcome(rawValue: rawOutcome)
|
|
226
|
+
else { return nil }
|
|
227
|
+
guard let rawNetworks = data["networks"] as? [String], outcome == .confirmed || rawNetworks.isEmpty else { return nil }
|
|
228
|
+
var seen = Set<String>()
|
|
229
|
+
let networks = rawNetworks.filter { network in
|
|
230
|
+
!network.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
231
|
+
&& seen.insert(network).inserted
|
|
232
|
+
}
|
|
233
|
+
return SavedWifiNetworksResult(
|
|
234
|
+
outcome: outcome,
|
|
235
|
+
networks: networks,
|
|
236
|
+
error: (data["error"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
func legacyWifiForgetResult(
|
|
241
|
+
ssid: String
|
|
242
|
+
) -> WifiForgetResult {
|
|
243
|
+
return WifiForgetResult(
|
|
244
|
+
ssid: ssid,
|
|
245
|
+
outcome: .legacyUnverified,
|
|
246
|
+
connected: nil,
|
|
247
|
+
currentSsid: nil,
|
|
248
|
+
localIp: nil,
|
|
249
|
+
error: nil
|
|
250
|
+
)
|
|
251
|
+
}
|
|
@@ -2796,7 +2796,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2796
2796
|
// Record the session id BEFORE marking ready: an unsolicited glasses_ready
|
|
2797
2797
|
// already runs this full remote-reset flow, so recording (not re-triggering)
|
|
2798
2798
|
// is correct here; version_info detection covers the restart case.
|
|
2799
|
-
|
|
2799
|
+
glassesSessionId = (json["sid"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
2800
|
+
Bridge.sendTypedMessage(
|
|
2801
|
+
"wifi_protocol_session_ready",
|
|
2802
|
+
body: ["sid": glassesSessionId ?? ""]
|
|
2803
|
+
)
|
|
2800
2804
|
readinessCompletedThisBleSession = true
|
|
2801
2805
|
handleGlassesReady()
|
|
2802
2806
|
|
|
@@ -2834,6 +2838,33 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2834
2838
|
}
|
|
2835
2839
|
updateWifiStatus(connected: connected, ssid: ssid, ip: ip, error: wifiError.isEmpty ? nil : wifiError)
|
|
2836
2840
|
|
|
2841
|
+
case "wifi_forget_result":
|
|
2842
|
+
guard wifiResponseEnvelopeIsValid(json, allowLegacy: true) else { return }
|
|
2843
|
+
Bridge.sendWifiForgetResult(
|
|
2844
|
+
requestId: json["requestId"] as? String ?? "",
|
|
2845
|
+
sid: json["sid"] as? String ?? "",
|
|
2846
|
+
ssid: json["ssid"] as? String ?? "",
|
|
2847
|
+
protocolVersion: (json["protocol_version"] as? NSNumber)?.intValue ?? 0,
|
|
2848
|
+
outcome: json["outcome"] as? String ?? "",
|
|
2849
|
+
legacyDispatched: json["dispatched"] as? Bool,
|
|
2850
|
+
connected: json["connected"] as? Bool,
|
|
2851
|
+
currentSsid: json["current_ssid"] as? String ?? "",
|
|
2852
|
+
localIp: json["local_ip"] as? String ?? "",
|
|
2853
|
+
error: (json["error"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
2854
|
+
)
|
|
2855
|
+
|
|
2856
|
+
case "saved_wifi_networks":
|
|
2857
|
+
guard wifiResponseEnvelopeIsValid(json, allowLegacy: false) else { return }
|
|
2858
|
+
guard let networks = json["networks"] as? [String] else { return }
|
|
2859
|
+
Bridge.sendSavedWifiNetworks(
|
|
2860
|
+
requestId: json["requestId"] as? String ?? "",
|
|
2861
|
+
sid: json["sid"] as? String ?? "",
|
|
2862
|
+
protocolVersion: (json["protocol_version"] as? NSNumber)?.intValue ?? 0,
|
|
2863
|
+
outcome: json["outcome"] as? String ?? "",
|
|
2864
|
+
networks: networks,
|
|
2865
|
+
error: (json["error"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
2866
|
+
)
|
|
2867
|
+
|
|
2837
2868
|
case "hotspot_status_update":
|
|
2838
2869
|
let enabled = json["hotspot_enabled"] as? Bool ?? false
|
|
2839
2870
|
let ssid = json["hotspot_ssid"] as? String ?? ""
|
|
@@ -2942,7 +2973,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2942
2973
|
Bridge.log("LIVE: Received pong response - connection healthy")
|
|
2943
2974
|
|
|
2944
2975
|
case "entering_pairing_mode":
|
|
2945
|
-
let windowMs = max(
|
|
2976
|
+
let windowMs = max(5000, min(180_000, json["window_ms"] as? Int ?? 120_000))
|
|
2946
2977
|
Bridge.log("LIVE: Glasses entering pairing mode — yield \(windowMs)ms (no forget)")
|
|
2947
2978
|
enterPairingYield(windowMs: windowMs)
|
|
2948
2979
|
let body: [String: Any] = [
|
|
@@ -3086,6 +3117,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3086
3117
|
|
|
3087
3118
|
// Extract all fields from JSON (except "type")
|
|
3088
3119
|
var fields: [String: Any] = [:]
|
|
3120
|
+
fields["version_info_type"] = type
|
|
3089
3121
|
for (key, value) in json {
|
|
3090
3122
|
if key != "type" {
|
|
3091
3123
|
fields[key] = value
|
|
@@ -3647,19 +3679,67 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3647
3679
|
}
|
|
3648
3680
|
|
|
3649
3681
|
func forgetWifiNetwork(_ ssid: String) {
|
|
3650
|
-
|
|
3682
|
+
forgetWifiNetwork(ssid, requestId: nil, sid: nil)
|
|
3683
|
+
}
|
|
3651
3684
|
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3685
|
+
@discardableResult func forgetWifiNetwork(_ ssid: String, requestId: String?, sid: String?) -> Bool {
|
|
3686
|
+
guard let peripheral = connectedPeripheral, peripheral.state == .connected, txCharacteristic != nil,
|
|
3687
|
+
(requestId == nil) == (sid == nil)
|
|
3688
|
+
else { return false }
|
|
3689
|
+
if let requestId, let sid, requestId.isEmpty || sid.isEmpty { return false }
|
|
3690
|
+
transportLog("LIVE: 📶 Sending WiFi forget command for SSID: \(ssid)", bridgeLogging: false)
|
|
3691
|
+
|
|
3692
|
+
guard !ssid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
3693
|
+
transportLog("LIVE: Cannot forget WiFi network - SSID is empty", bridgeLogging: false)
|
|
3694
|
+
if let requestId {
|
|
3695
|
+
Bridge.sendWifiForgetResult(
|
|
3696
|
+
requestId: requestId,
|
|
3697
|
+
sid: sid ?? "",
|
|
3698
|
+
ssid: ssid,
|
|
3699
|
+
protocolVersion: 1,
|
|
3700
|
+
outcome: "failed",
|
|
3701
|
+
legacyDispatched: nil,
|
|
3702
|
+
connected: nil,
|
|
3703
|
+
currentSsid: "",
|
|
3704
|
+
localIp: "",
|
|
3705
|
+
error: "invalid_ssid"
|
|
3706
|
+
)
|
|
3707
|
+
}
|
|
3708
|
+
return false
|
|
3655
3709
|
}
|
|
3656
3710
|
|
|
3657
|
-
|
|
3711
|
+
var json: [String: Any] = [
|
|
3658
3712
|
"type": "forget_wifi",
|
|
3659
3713
|
"ssid": ssid,
|
|
3660
3714
|
]
|
|
3715
|
+
if let requestId, !requestId.isEmpty {
|
|
3716
|
+
json["requestId"] = requestId
|
|
3717
|
+
}
|
|
3718
|
+
if let sid, !sid.isEmpty {
|
|
3719
|
+
json["sid"] = sid
|
|
3720
|
+
json["protocolVersion"] = 1
|
|
3721
|
+
}
|
|
3661
3722
|
|
|
3662
|
-
|
|
3723
|
+
// The coordinator dispatches while isolated on MainActor. Keep this path native-only so a
|
|
3724
|
+
// public log delegate cannot synchronously reset the session before the payload is queued.
|
|
3725
|
+
return sendJson(json, wakeUp: true, requireAck: true, bridgeLogging: false)
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
@discardableResult func requestSavedWifiNetworks(requestId: String, sid: String) -> Bool {
|
|
3729
|
+
guard let peripheral = connectedPeripheral, peripheral.state == .connected, txCharacteristic != nil,
|
|
3730
|
+
!requestId.isEmpty, !sid.isEmpty
|
|
3731
|
+
else { return false }
|
|
3732
|
+
var command: [String: Any] = ["type": "request_saved_wifi_networks", "requestId": requestId]
|
|
3733
|
+
command["protocolVersion"] = 1
|
|
3734
|
+
if !sid.isEmpty {
|
|
3735
|
+
command["sid"] = sid
|
|
3736
|
+
}
|
|
3737
|
+
return sendJson(
|
|
3738
|
+
command,
|
|
3739
|
+
wakeUp: true,
|
|
3740
|
+
requireAck: true,
|
|
3741
|
+
bridgeLogging: false
|
|
3742
|
+
)
|
|
3663
3743
|
}
|
|
3664
3744
|
|
|
3665
3745
|
func queryGalleryStatus() {
|
|
@@ -4822,20 +4902,31 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4822
4902
|
wireBytes: Int,
|
|
4823
4903
|
packetCount: Int,
|
|
4824
4904
|
protocolVersion: Int,
|
|
4825
|
-
direction: String
|
|
4905
|
+
direction: String,
|
|
4906
|
+
bridgeLogging: Bool = true
|
|
4826
4907
|
) {
|
|
4827
|
-
|
|
4828
|
-
"BLE_TRACE direction=\(direction) proto=v\(protocolVersion) payload=\(payloadBytes) wire=\(wireBytes) packets=\(packetCount)"
|
|
4908
|
+
transportLog(
|
|
4909
|
+
"BLE_TRACE direction=\(direction) proto=v\(protocolVersion) payload=\(payloadBytes) wire=\(wireBytes) packets=\(packetCount)",
|
|
4910
|
+
bridgeLogging: bridgeLogging
|
|
4829
4911
|
)
|
|
4830
4912
|
}
|
|
4831
4913
|
|
|
4832
|
-
private func
|
|
4914
|
+
private func transportLog(_ message: String, bridgeLogging: Bool) {
|
|
4915
|
+
if bridgeLogging {
|
|
4916
|
+
Bridge.log(message)
|
|
4917
|
+
} else {
|
|
4918
|
+
NSLog("%@", message)
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4922
|
+
@discardableResult private func sendJsonBinary(
|
|
4833
4923
|
jsonString: String,
|
|
4834
4924
|
messageId: Int64,
|
|
4835
4925
|
trackingId: String,
|
|
4836
4926
|
wakeUp: Bool,
|
|
4837
|
-
requireAck: Bool
|
|
4838
|
-
|
|
4927
|
+
requireAck: Bool,
|
|
4928
|
+
bridgeLogging: Bool
|
|
4929
|
+
) -> Bool {
|
|
4839
4930
|
let payload = Data(jsonString.utf8)
|
|
4840
4931
|
let msgId = UInt16(truncatingIfNeeded: messageId)
|
|
4841
4932
|
let fragments = MessageChunker.createBinaryFragments(
|
|
@@ -4845,8 +4936,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4845
4936
|
ackRequested: requireAck && messageId >= 0
|
|
4846
4937
|
)
|
|
4847
4938
|
guard !fragments.isEmpty else {
|
|
4848
|
-
|
|
4849
|
-
return
|
|
4939
|
+
transportLog("LIVE: Failed to create binary wire fragments", bridgeLogging: bridgeLogging)
|
|
4940
|
+
return false
|
|
4850
4941
|
}
|
|
4851
4942
|
|
|
4852
4943
|
var totalWireBytes = 0
|
|
@@ -4858,7 +4949,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4858
4949
|
fragCount: fragment.fragCount,
|
|
4859
4950
|
payload: fragment.payload
|
|
4860
4951
|
) else {
|
|
4861
|
-
|
|
4952
|
+
return false
|
|
4862
4953
|
}
|
|
4863
4954
|
totalWireBytes += packed.count
|
|
4864
4955
|
let isFinalChunk = index == fragments.count - 1
|
|
@@ -4875,12 +4966,30 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4875
4966
|
wireBytes: totalWireBytes,
|
|
4876
4967
|
packetCount: fragments.count,
|
|
4877
4968
|
protocolVersion: BleWireProtocol.protocolV2,
|
|
4878
|
-
direction: "phone_to_glasses"
|
|
4969
|
+
direction: "phone_to_glasses",
|
|
4970
|
+
bridgeLogging: bridgeLogging
|
|
4971
|
+
)
|
|
4972
|
+
transportLog(
|
|
4973
|
+
"LIVE: Binary v2 queued \(fragments.count) fragments, wireBytes=\(totalWireBytes)",
|
|
4974
|
+
bridgeLogging: bridgeLogging
|
|
4879
4975
|
)
|
|
4880
|
-
|
|
4976
|
+
return true
|
|
4881
4977
|
}
|
|
4882
4978
|
|
|
4883
|
-
func sendJson(
|
|
4979
|
+
func sendJson(
|
|
4980
|
+
_ jsonOriginal: [String: Any],
|
|
4981
|
+
wakeUp: Bool = false,
|
|
4982
|
+
requireAck: Bool = true
|
|
4983
|
+
) {
|
|
4984
|
+
sendJson(jsonOriginal, wakeUp: wakeUp, requireAck: requireAck, bridgeLogging: true)
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4987
|
+
@discardableResult private func sendJson(
|
|
4988
|
+
_ jsonOriginal: [String: Any],
|
|
4989
|
+
wakeUp: Bool,
|
|
4990
|
+
requireAck: Bool,
|
|
4991
|
+
bridgeLogging: Bool
|
|
4992
|
+
) -> Bool {
|
|
4884
4993
|
do {
|
|
4885
4994
|
var json = jsonOriginal
|
|
4886
4995
|
var messageId: Int64 = -1
|
|
@@ -4904,14 +5013,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4904
5013
|
)
|
|
4905
5014
|
|
|
4906
5015
|
if useBinaryWireProtocol, isNewVersion {
|
|
4907
|
-
sendJsonBinary(
|
|
5016
|
+
return sendJsonBinary(
|
|
4908
5017
|
jsonString: jsonString,
|
|
4909
5018
|
messageId: messageId,
|
|
4910
5019
|
trackingId: trackingId,
|
|
4911
5020
|
wakeUp: wakeUp,
|
|
4912
|
-
requireAck: requireAck
|
|
5021
|
+
requireAck: requireAck,
|
|
5022
|
+
bridgeLogging: bridgeLogging
|
|
4913
5023
|
)
|
|
4914
|
-
return
|
|
4915
5024
|
}
|
|
4916
5025
|
|
|
4917
5026
|
// First check if the message needs chunking
|
|
@@ -4925,17 +5034,20 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4925
5034
|
|
|
4926
5035
|
// Check if chunking is needed
|
|
4927
5036
|
if MessageChunker.needsChunking(testWrappedJson) {
|
|
4928
|
-
|
|
5037
|
+
transportLog("LIVE: Message exceeds threshold, chunking required", bridgeLogging: bridgeLogging)
|
|
4929
5038
|
|
|
4930
5039
|
// Create chunks
|
|
4931
5040
|
let chunks = MessageChunker.createChunks(
|
|
4932
5041
|
originalJson: jsonString, messageId: messageId, wakeUp: wakeUp
|
|
4933
5042
|
)
|
|
4934
5043
|
guard !chunks.isEmpty else {
|
|
4935
|
-
|
|
4936
|
-
|
|
5044
|
+
transportLog(
|
|
5045
|
+
"LIVE: Failed to create BLE chunks within K900 packet limit",
|
|
5046
|
+
bridgeLogging: bridgeLogging
|
|
5047
|
+
)
|
|
5048
|
+
return false
|
|
4937
5049
|
}
|
|
4938
|
-
|
|
5050
|
+
transportLog("LIVE: Sending \(chunks.count) chunks", bridgeLogging: bridgeLogging)
|
|
4939
5051
|
|
|
4940
5052
|
// Send each chunk
|
|
4941
5053
|
for (index, chunk) in chunks.enumerated() {
|
|
@@ -4943,7 +5055,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4943
5055
|
if let chunkStr = String(data: chunkData, encoding: .utf8) {
|
|
4944
5056
|
// Pack each chunk using the normal K900 protocol
|
|
4945
5057
|
let packedData =
|
|
4946
|
-
packJson(
|
|
5058
|
+
packJson(
|
|
5059
|
+
chunkStr,
|
|
5060
|
+
wakeUp: wakeUp && index == 0,
|
|
5061
|
+
bridgeLogging: bridgeLogging
|
|
5062
|
+
) ?? Data() // Only wakeup on first chunk
|
|
4947
5063
|
|
|
4948
5064
|
// Queue the chunk for sending
|
|
4949
5065
|
// Only track ACK for the final chunk (which has the mId)
|
|
@@ -4964,6 +5080,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4964
5080
|
"chunkJsonBytes": chunkStr.data(using: .utf8)?.count,
|
|
4965
5081
|
"messageBytes": jsonData.count,
|
|
4966
5082
|
])
|
|
5083
|
+
guard !packedData.isEmpty else { return false }
|
|
4967
5084
|
queueSend(packedData, id: chunkTrackingId, trace: trace)
|
|
4968
5085
|
|
|
4969
5086
|
// Add small delay between chunks to avoid overwhelming the connection
|
|
@@ -4973,14 +5090,22 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4973
5090
|
}
|
|
4974
5091
|
}
|
|
4975
5092
|
|
|
4976
|
-
|
|
5093
|
+
transportLog("LIVE: All chunks queued for transmission", bridgeLogging: bridgeLogging)
|
|
4977
5094
|
} else {
|
|
4978
5095
|
// Normal single message transmission
|
|
4979
5096
|
if (json["type"] as? String) == "take_photo" {
|
|
4980
|
-
|
|
5097
|
+
transportLog(
|
|
5098
|
+
"LIVE: PHOTO PIPELINE BLE handoff — sendJson -> queueSend take_photo",
|
|
5099
|
+
bridgeLogging: bridgeLogging
|
|
5100
|
+
)
|
|
4981
5101
|
}
|
|
4982
|
-
|
|
4983
|
-
let packedData =
|
|
5102
|
+
transportLog("LIVE: Sending data to glasses: \(jsonString)", bridgeLogging: bridgeLogging)
|
|
5103
|
+
let packedData =
|
|
5104
|
+
packJson(
|
|
5105
|
+
jsonString,
|
|
5106
|
+
wakeUp: wakeUp,
|
|
5107
|
+
bridgeLogging: bridgeLogging
|
|
5108
|
+
) ?? Data()
|
|
4984
5109
|
let trace = createBleWriteTrace(
|
|
4985
5110
|
commandInfo: commandInfo,
|
|
4986
5111
|
chunkId: nil,
|
|
@@ -4994,12 +5119,15 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4994
5119
|
logBleChunkTrace("created", trace, extra: [
|
|
4995
5120
|
"messageBytes": jsonData.count,
|
|
4996
5121
|
])
|
|
5122
|
+
guard !packedData.isEmpty else { return false }
|
|
4997
5123
|
queueSend(packedData, id: trackingId, trace: trace)
|
|
4998
5124
|
}
|
|
5125
|
+
return true
|
|
4999
5126
|
}
|
|
5000
5127
|
} catch {
|
|
5001
|
-
|
|
5128
|
+
transportLog("LIVE: Error creating JSON: \(error)", bridgeLogging: bridgeLogging)
|
|
5002
5129
|
}
|
|
5130
|
+
return false
|
|
5003
5131
|
}
|
|
5004
5132
|
|
|
5005
5133
|
// MARK: - Status Requests
|
|
@@ -5990,7 +6118,11 @@ extension MentraLive {
|
|
|
5990
6118
|
* 1. Wrap with C-field: {"C": jsonData}
|
|
5991
6119
|
* 2. Then pack with BES2700 protocol using little-endian: ## + type + length + {"C": jsonData} + $$
|
|
5992
6120
|
*/
|
|
5993
|
-
private func packJson(
|
|
6121
|
+
private func packJson(
|
|
6122
|
+
_ jsonData: String?,
|
|
6123
|
+
wakeUp: Bool = false,
|
|
6124
|
+
bridgeLogging: Bool = true
|
|
6125
|
+
) -> Data? {
|
|
5994
6126
|
guard let jsonData else { return nil }
|
|
5995
6127
|
|
|
5996
6128
|
do {
|
|
@@ -6013,7 +6145,10 @@ extension MentraLive {
|
|
|
6013
6145
|
)
|
|
6014
6146
|
|
|
6015
6147
|
} catch {
|
|
6016
|
-
|
|
6148
|
+
transportLog(
|
|
6149
|
+
"Error creating JSON wrapper for K900: \(error)",
|
|
6150
|
+
bridgeLogging: bridgeLogging
|
|
6151
|
+
)
|
|
6017
6152
|
return nil
|
|
6018
6153
|
}
|
|
6019
6154
|
}
|
|
@@ -195,8 +195,10 @@ protocol SGCManager {
|
|
|
195
195
|
// MARK: - Network Management
|
|
196
196
|
|
|
197
197
|
func requestWifiScan(scanId: String?)
|
|
198
|
+
@discardableResult func requestSavedWifiNetworks(requestId: String, sid: String) -> Bool
|
|
198
199
|
func sendWifiCredentials(_ ssid: String, _ password: String)
|
|
199
200
|
func forgetWifiNetwork(_ ssid: String)
|
|
201
|
+
@discardableResult func forgetWifiNetwork(_ ssid: String, requestId: String?, sid: String?) -> Bool
|
|
200
202
|
func sendHotspotState(_ enabled: Bool)
|
|
201
203
|
func sendWifiAdbState(_ enabled: Bool)
|
|
202
204
|
func sendOtaStart(otaVersionUrl: String?)
|
|
@@ -232,7 +234,17 @@ protocol SGCManager {
|
|
|
232
234
|
/// doesn't seem to work for concurrency reasons :(
|
|
233
235
|
/// we can make read-only getters for convienence though:
|
|
234
236
|
extension SGCManager {
|
|
235
|
-
var isMicSuspendedForAudio: Bool {
|
|
237
|
+
var isMicSuspendedForAudio: Bool {
|
|
238
|
+
false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@discardableResult func requestSavedWifiNetworks(requestId _: String, sid _: String) -> Bool {
|
|
242
|
+
false
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@discardableResult func forgetWifiNetwork(_: String, requestId _: String?, sid _: String?) -> Bool {
|
|
246
|
+
false
|
|
247
|
+
}
|
|
236
248
|
|
|
237
249
|
/// Default: no-op. Only G2 renders positioned text containers; other glasses ignore it.
|
|
238
250
|
func sendPositionedText(
|