@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.
Files changed (50) hide show
  1. package/README.md +46 -45
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -1
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +63 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +5 -4
  5. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
  6. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  7. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +285 -29
  8. package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +312 -45
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -0
  14. package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
  15. package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
  16. package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
  17. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
  18. package/build/BluetoothSdk.types.d.ts +62 -1
  19. package/build/BluetoothSdk.types.d.ts.map +1 -1
  20. package/build/BluetoothSdk.types.js.map +1 -1
  21. package/build/_private/BluetoothSdkModule.d.ts +3 -2
  22. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  23. package/build/_private/BluetoothSdkModule.js.map +1 -1
  24. package/build/generated/changelogCatalog.d.ts +1 -1
  25. package/build/generated/changelogCatalog.js +1 -1
  26. package/build/generated/changelogCatalog.js.map +1 -1
  27. package/build/generated/releaseMetadata.js +5 -5
  28. package/build/generated/releaseMetadata.js.map +1 -1
  29. package/build/index.d.ts +1 -1
  30. package/build/index.d.ts.map +1 -1
  31. package/build/index.js +3 -0
  32. package/build/index.js.map +1 -1
  33. package/ios/BluetoothSdkModule.swift +8 -2
  34. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  35. package/ios/Source/Bridge.swift +63 -0
  36. package/ios/Source/DeviceManager.swift +6 -3
  37. package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
  38. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  39. package/ios/Source/MentraBluetoothSDK.swift +365 -93
  40. package/ios/Source/WifiRequestResolution.swift +251 -0
  41. package/ios/Source/sgcs/MentraLive.swift +170 -35
  42. package/ios/Source/sgcs/SGCManager.swift +13 -1
  43. package/ios/Source/status/DeviceStatus.swift +28 -0
  44. package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
  45. package/package.json +1 -1
  46. package/src/BluetoothSdk.types.ts +76 -1
  47. package/src/_private/BluetoothSdkModule.ts +4 -1
  48. package/src/generated/changelogCatalog.ts +1 -1
  49. package/src/generated/releaseMetadata.ts +5 -5
  50. 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
- if let sid = json["sid"] as? String, !sid.isEmpty { glassesSessionId = sid }
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(5_000, min(180_000, json["window_ms"] as? Int ?? 120_000))
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
- Bridge.log("LIVE: 📶 Sending WiFi forget command for SSID: \(ssid)")
3682
+ forgetWifiNetwork(ssid, requestId: nil, sid: nil)
3683
+ }
3651
3684
 
3652
- guard !ssid.isEmpty else {
3653
- Bridge.log("LIVE: Cannot forget WiFi network - SSID is empty")
3654
- return
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
- let json: [String: Any] = [
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
- sendJson(json, wakeUp: true)
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
- Bridge.log(
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 sendJsonBinary(
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
- Bridge.log("LIVE: Failed to create binary wire fragments")
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
- continue
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
- Bridge.log("LIVE: Binary v2 queued \(fragments.count) fragments, wireBytes=\(totalWireBytes)")
4976
+ return true
4881
4977
  }
4882
4978
 
4883
- func sendJson(_ jsonOriginal: [String: Any], wakeUp: Bool = false, requireAck: Bool = true) {
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
- Bridge.log("LIVE: Message exceeds threshold, chunking required")
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
- Bridge.log("LIVE: Failed to create BLE chunks within K900 packet limit")
4936
- return
5044
+ transportLog(
5045
+ "LIVE: Failed to create BLE chunks within K900 packet limit",
5046
+ bridgeLogging: bridgeLogging
5047
+ )
5048
+ return false
4937
5049
  }
4938
- Bridge.log("LIVE: Sending \(chunks.count) chunks")
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(chunkStr, wakeUp: wakeUp && index == 0) ?? Data() // Only wakeup on first chunk
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
- Bridge.log("LIVE: All chunks queued for transmission")
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
- Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — sendJson -> queueSend take_photo")
5097
+ transportLog(
5098
+ "LIVE: PHOTO PIPELINE BLE handoff — sendJson -> queueSend take_photo",
5099
+ bridgeLogging: bridgeLogging
5100
+ )
4981
5101
  }
4982
- Bridge.log("LIVE: Sending data to glasses: \(jsonString)")
4983
- let packedData = packJson(jsonString, wakeUp: wakeUp) ?? Data()
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
- Bridge.log("LIVE: Error creating JSON: \(error)")
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(_ jsonData: String?, wakeUp: Bool = false) -> Data? {
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
- Bridge.log("Error creating JSON wrapper for K900: \(error)")
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 { false }
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(