@mentra/bluetooth-sdk 3.2.0-dev.200 → 3.2.0-dev.206
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +63 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +5 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +298 -181
- package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +326 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbe.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamSessionState.kt +32 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbeTest.kt +24 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamSessionStateTest.kt +43 -0
- package/build/BluetoothSdk.types.d.ts +68 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +3 -4
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +8 -13
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +63 -0
- package/ios/Source/DeviceManager.swift +6 -3
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +372 -220
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/sgcs/MentraLive.swift +216 -70
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +28 -0
- package/ios/Source/streaming/StreamControllerProbe.swift +18 -0
- package/ios/Source/streaming/StreamModels.swift +12 -0
- package/ios/Source/streaming/StreamSessionState.swift +29 -0
- package/ios/Tests/StreamControllerProbeTests.swift +27 -0
- package/ios/Tests/StreamSessionStateTests.swift +38 -0
- package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +82 -2
- package/src/_private/BluetoothSdkModule.ts +4 -4
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +11 -0
|
@@ -1022,17 +1022,17 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
1022
1022
|
}
|
|
1023
1023
|
|
|
1024
1024
|
#if !os(macOS)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1025
|
+
nonisolated func centralManager(
|
|
1026
|
+
_: CBCentralManager, didUpdateANCSAuthorizationFor peripheral: CBPeripheral
|
|
1027
|
+
) {
|
|
1028
|
+
DispatchQueue.main.async { [weak self] in
|
|
1029
|
+
guard let self, peripheral === self.connectedPeripheral else { return }
|
|
1030
|
+
Bridge.log(
|
|
1031
|
+
"LIVE: ANCS authorization updated: \(peripheral.ancsAuthorized ? "authorized" : "not authorized")"
|
|
1032
|
+
)
|
|
1033
|
+
self.enableAncsRelayIfAuthorized()
|
|
1034
|
+
}
|
|
1034
1035
|
}
|
|
1035
|
-
}
|
|
1036
1036
|
#endif
|
|
1037
1037
|
|
|
1038
1038
|
nonisolated func centralManager(
|
|
@@ -2154,6 +2154,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2154
2154
|
Bridge.log("Starting stream")
|
|
2155
2155
|
var json = message
|
|
2156
2156
|
json.removeValue(forKey: "timestamp")
|
|
2157
|
+
json["controllerProbeVersion"] = 1
|
|
2158
|
+
json["controllerId"] = StreamControllerProbe.controllerId
|
|
2157
2159
|
sendJson(json, wakeUp: true)
|
|
2158
2160
|
}
|
|
2159
2161
|
|
|
@@ -2461,16 +2463,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2461
2463
|
startConnectionTimeout()
|
|
2462
2464
|
|
|
2463
2465
|
#if os(macOS)
|
|
2464
|
-
|
|
2466
|
+
centralManager?.connect(peripheral, options: nil)
|
|
2465
2467
|
#else
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2468
|
+
// ANCS is hosted by iOS and is only exposed to authorized accessories.
|
|
2469
|
+
// The default requirement lets the system complete that authorization flow
|
|
2470
|
+
// before the glasses subscribe; apps that do not relay notifications can opt out.
|
|
2471
|
+
Bridge.log("LIVE: ANCS connection requirement \(requiresAncs ? "enabled" : "disabled")")
|
|
2472
|
+
centralManager?.connect(
|
|
2473
|
+
peripheral,
|
|
2474
|
+
options: MentraLiveConnectionOptions.coreBluetoothOptions(requiresAncs: requiresAncs)
|
|
2475
|
+
)
|
|
2474
2476
|
#endif
|
|
2475
2477
|
}
|
|
2476
2478
|
|
|
@@ -2479,23 +2481,23 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2479
2481
|
/// for old mobile clients that never send it.
|
|
2480
2482
|
private func enableAncsRelayIfAuthorized() {
|
|
2481
2483
|
#if !os(macOS)
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2484
|
+
guard !ancsRelayEnableRequested,
|
|
2485
|
+
let peripheral = connectedPeripheral,
|
|
2486
|
+
txCharacteristic != nil,
|
|
2487
|
+
rxCharacteristic?.isNotifying == true,
|
|
2488
|
+
peripheral.ancsAuthorized
|
|
2489
|
+
else {
|
|
2490
|
+
return
|
|
2491
|
+
}
|
|
2490
2492
|
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2493
|
+
let command: [String: Any] = [
|
|
2494
|
+
"C": "cs_ancs",
|
|
2495
|
+
"B": ["enabled": 1],
|
|
2496
|
+
]
|
|
2497
|
+
if sendRawK900Command(command) {
|
|
2498
|
+
ancsRelayEnableRequested = true
|
|
2499
|
+
Bridge.log("LIVE: Requested ANCS relay from compatible firmware")
|
|
2500
|
+
}
|
|
2499
2501
|
#endif
|
|
2500
2502
|
}
|
|
2501
2503
|
|
|
@@ -2796,8 +2798,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2796
2798
|
// Record the session id BEFORE marking ready: an unsolicited glasses_ready
|
|
2797
2799
|
// already runs this full remote-reset flow, so recording (not re-triggering)
|
|
2798
2800
|
// is correct here; version_info detection covers the restart case.
|
|
2799
|
-
|
|
2801
|
+
glassesSessionId = (json["sid"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
2802
|
+
Bridge.sendTypedMessage(
|
|
2803
|
+
"wifi_protocol_session_ready",
|
|
2804
|
+
body: ["sid": glassesSessionId ?? ""]
|
|
2805
|
+
)
|
|
2800
2806
|
readinessCompletedThisBleSession = true
|
|
2807
|
+
Bridge.sendTypedMessage("stream_control_ready", body: [
|
|
2808
|
+
"sid": json["sid"] as? String ?? "",
|
|
2809
|
+
"streamControlVersion": json["streamControlVersion"] as? Int ?? 0,
|
|
2810
|
+
])
|
|
2801
2811
|
handleGlassesReady()
|
|
2802
2812
|
|
|
2803
2813
|
case "battery_status":
|
|
@@ -2834,6 +2844,33 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2834
2844
|
}
|
|
2835
2845
|
updateWifiStatus(connected: connected, ssid: ssid, ip: ip, error: wifiError.isEmpty ? nil : wifiError)
|
|
2836
2846
|
|
|
2847
|
+
case "wifi_forget_result":
|
|
2848
|
+
guard wifiResponseEnvelopeIsValid(json, allowLegacy: true) else { return }
|
|
2849
|
+
Bridge.sendWifiForgetResult(
|
|
2850
|
+
requestId: json["requestId"] as? String ?? "",
|
|
2851
|
+
sid: json["sid"] as? String ?? "",
|
|
2852
|
+
ssid: json["ssid"] as? String ?? "",
|
|
2853
|
+
protocolVersion: (json["protocol_version"] as? NSNumber)?.intValue ?? 0,
|
|
2854
|
+
outcome: json["outcome"] as? String ?? "",
|
|
2855
|
+
legacyDispatched: json["dispatched"] as? Bool,
|
|
2856
|
+
connected: json["connected"] as? Bool,
|
|
2857
|
+
currentSsid: json["current_ssid"] as? String ?? "",
|
|
2858
|
+
localIp: json["local_ip"] as? String ?? "",
|
|
2859
|
+
error: (json["error"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
2860
|
+
)
|
|
2861
|
+
|
|
2862
|
+
case "saved_wifi_networks":
|
|
2863
|
+
guard wifiResponseEnvelopeIsValid(json, allowLegacy: false) else { return }
|
|
2864
|
+
guard let networks = json["networks"] as? [String] else { return }
|
|
2865
|
+
Bridge.sendSavedWifiNetworks(
|
|
2866
|
+
requestId: json["requestId"] as? String ?? "",
|
|
2867
|
+
sid: json["sid"] as? String ?? "",
|
|
2868
|
+
protocolVersion: (json["protocol_version"] as? NSNumber)?.intValue ?? 0,
|
|
2869
|
+
outcome: json["outcome"] as? String ?? "",
|
|
2870
|
+
networks: networks,
|
|
2871
|
+
error: (json["error"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
2872
|
+
)
|
|
2873
|
+
|
|
2837
2874
|
case "hotspot_status_update":
|
|
2838
2875
|
let enabled = json["hotspot_enabled"] as? Bool ?? false
|
|
2839
2876
|
let ssid = json["hotspot_ssid"] as? String ?? ""
|
|
@@ -2938,11 +2975,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2938
2975
|
let error = json["errorCode"] as? String ?? json["error"] as? String
|
|
2939
2976
|
Bridge.sendRgbLedControlResponse(requestId: requestId, success: success, error: error)
|
|
2940
2977
|
|
|
2978
|
+
case "stream_controller_probe":
|
|
2979
|
+
if let response = StreamControllerProbe.response(json) {
|
|
2980
|
+
sendJson(response)
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2941
2983
|
case "pong":
|
|
2942
2984
|
Bridge.log("LIVE: Received pong response - connection healthy")
|
|
2943
2985
|
|
|
2944
2986
|
case "entering_pairing_mode":
|
|
2945
|
-
let windowMs = max(
|
|
2987
|
+
let windowMs = max(5000, min(180_000, json["window_ms"] as? Int ?? 120_000))
|
|
2946
2988
|
Bridge.log("LIVE: Glasses entering pairing mode — yield \(windowMs)ms (no forget)")
|
|
2947
2989
|
enterPairingYield(windowMs: windowMs)
|
|
2948
2990
|
let body: [String: Any] = [
|
|
@@ -3086,6 +3128,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3086
3128
|
|
|
3087
3129
|
// Extract all fields from JSON (except "type")
|
|
3088
3130
|
var fields: [String: Any] = [:]
|
|
3131
|
+
fields["version_info_type"] = type
|
|
3089
3132
|
for (key, value) in json {
|
|
3090
3133
|
if key != "type" {
|
|
3091
3134
|
fields[key] = value
|
|
@@ -3647,19 +3690,67 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3647
3690
|
}
|
|
3648
3691
|
|
|
3649
3692
|
func forgetWifiNetwork(_ ssid: String) {
|
|
3650
|
-
|
|
3693
|
+
forgetWifiNetwork(ssid, requestId: nil, sid: nil)
|
|
3694
|
+
}
|
|
3651
3695
|
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3696
|
+
@discardableResult func forgetWifiNetwork(_ ssid: String, requestId: String?, sid: String?) -> Bool {
|
|
3697
|
+
guard let peripheral = connectedPeripheral, peripheral.state == .connected, txCharacteristic != nil,
|
|
3698
|
+
(requestId == nil) == (sid == nil)
|
|
3699
|
+
else { return false }
|
|
3700
|
+
if let requestId, let sid, requestId.isEmpty || sid.isEmpty { return false }
|
|
3701
|
+
transportLog("LIVE: 📶 Sending WiFi forget command for SSID: \(ssid)", bridgeLogging: false)
|
|
3702
|
+
|
|
3703
|
+
guard !ssid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
3704
|
+
transportLog("LIVE: Cannot forget WiFi network - SSID is empty", bridgeLogging: false)
|
|
3705
|
+
if let requestId {
|
|
3706
|
+
Bridge.sendWifiForgetResult(
|
|
3707
|
+
requestId: requestId,
|
|
3708
|
+
sid: sid ?? "",
|
|
3709
|
+
ssid: ssid,
|
|
3710
|
+
protocolVersion: 1,
|
|
3711
|
+
outcome: "failed",
|
|
3712
|
+
legacyDispatched: nil,
|
|
3713
|
+
connected: nil,
|
|
3714
|
+
currentSsid: "",
|
|
3715
|
+
localIp: "",
|
|
3716
|
+
error: "invalid_ssid"
|
|
3717
|
+
)
|
|
3718
|
+
}
|
|
3719
|
+
return false
|
|
3655
3720
|
}
|
|
3656
3721
|
|
|
3657
|
-
|
|
3722
|
+
var json: [String: Any] = [
|
|
3658
3723
|
"type": "forget_wifi",
|
|
3659
3724
|
"ssid": ssid,
|
|
3660
3725
|
]
|
|
3726
|
+
if let requestId, !requestId.isEmpty {
|
|
3727
|
+
json["requestId"] = requestId
|
|
3728
|
+
}
|
|
3729
|
+
if let sid, !sid.isEmpty {
|
|
3730
|
+
json["sid"] = sid
|
|
3731
|
+
json["protocolVersion"] = 1
|
|
3732
|
+
}
|
|
3661
3733
|
|
|
3662
|
-
|
|
3734
|
+
// The coordinator dispatches while isolated on MainActor. Keep this path native-only so a
|
|
3735
|
+
// public log delegate cannot synchronously reset the session before the payload is queued.
|
|
3736
|
+
return sendJson(json, wakeUp: true, requireAck: true, bridgeLogging: false)
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
@discardableResult func requestSavedWifiNetworks(requestId: String, sid: String) -> Bool {
|
|
3740
|
+
guard let peripheral = connectedPeripheral, peripheral.state == .connected, txCharacteristic != nil,
|
|
3741
|
+
!requestId.isEmpty, !sid.isEmpty
|
|
3742
|
+
else { return false }
|
|
3743
|
+
var command: [String: Any] = ["type": "request_saved_wifi_networks", "requestId": requestId]
|
|
3744
|
+
command["protocolVersion"] = 1
|
|
3745
|
+
if !sid.isEmpty {
|
|
3746
|
+
command["sid"] = sid
|
|
3747
|
+
}
|
|
3748
|
+
return sendJson(
|
|
3749
|
+
command,
|
|
3750
|
+
wakeUp: true,
|
|
3751
|
+
requireAck: true,
|
|
3752
|
+
bridgeLogging: false
|
|
3753
|
+
)
|
|
3663
3754
|
}
|
|
3664
3755
|
|
|
3665
3756
|
func queryGalleryStatus() {
|
|
@@ -4822,20 +4913,31 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4822
4913
|
wireBytes: Int,
|
|
4823
4914
|
packetCount: Int,
|
|
4824
4915
|
protocolVersion: Int,
|
|
4825
|
-
direction: String
|
|
4916
|
+
direction: String,
|
|
4917
|
+
bridgeLogging: Bool = true
|
|
4826
4918
|
) {
|
|
4827
|
-
|
|
4828
|
-
"BLE_TRACE direction=\(direction) proto=v\(protocolVersion) payload=\(payloadBytes) wire=\(wireBytes) packets=\(packetCount)"
|
|
4919
|
+
transportLog(
|
|
4920
|
+
"BLE_TRACE direction=\(direction) proto=v\(protocolVersion) payload=\(payloadBytes) wire=\(wireBytes) packets=\(packetCount)",
|
|
4921
|
+
bridgeLogging: bridgeLogging
|
|
4829
4922
|
)
|
|
4830
4923
|
}
|
|
4831
4924
|
|
|
4832
|
-
private func
|
|
4925
|
+
private func transportLog(_ message: String, bridgeLogging: Bool) {
|
|
4926
|
+
if bridgeLogging {
|
|
4927
|
+
Bridge.log(message)
|
|
4928
|
+
} else {
|
|
4929
|
+
NSLog("%@", message)
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4933
|
+
@discardableResult private func sendJsonBinary(
|
|
4833
4934
|
jsonString: String,
|
|
4834
4935
|
messageId: Int64,
|
|
4835
4936
|
trackingId: String,
|
|
4836
4937
|
wakeUp: Bool,
|
|
4837
|
-
requireAck: Bool
|
|
4838
|
-
|
|
4938
|
+
requireAck: Bool,
|
|
4939
|
+
bridgeLogging: Bool
|
|
4940
|
+
) -> Bool {
|
|
4839
4941
|
let payload = Data(jsonString.utf8)
|
|
4840
4942
|
let msgId = UInt16(truncatingIfNeeded: messageId)
|
|
4841
4943
|
let fragments = MessageChunker.createBinaryFragments(
|
|
@@ -4845,8 +4947,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4845
4947
|
ackRequested: requireAck && messageId >= 0
|
|
4846
4948
|
)
|
|
4847
4949
|
guard !fragments.isEmpty else {
|
|
4848
|
-
|
|
4849
|
-
return
|
|
4950
|
+
transportLog("LIVE: Failed to create binary wire fragments", bridgeLogging: bridgeLogging)
|
|
4951
|
+
return false
|
|
4850
4952
|
}
|
|
4851
4953
|
|
|
4852
4954
|
var totalWireBytes = 0
|
|
@@ -4858,7 +4960,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4858
4960
|
fragCount: fragment.fragCount,
|
|
4859
4961
|
payload: fragment.payload
|
|
4860
4962
|
) else {
|
|
4861
|
-
|
|
4963
|
+
return false
|
|
4862
4964
|
}
|
|
4863
4965
|
totalWireBytes += packed.count
|
|
4864
4966
|
let isFinalChunk = index == fragments.count - 1
|
|
@@ -4875,12 +4977,30 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4875
4977
|
wireBytes: totalWireBytes,
|
|
4876
4978
|
packetCount: fragments.count,
|
|
4877
4979
|
protocolVersion: BleWireProtocol.protocolV2,
|
|
4878
|
-
direction: "phone_to_glasses"
|
|
4980
|
+
direction: "phone_to_glasses",
|
|
4981
|
+
bridgeLogging: bridgeLogging
|
|
4879
4982
|
)
|
|
4880
|
-
|
|
4983
|
+
transportLog(
|
|
4984
|
+
"LIVE: Binary v2 queued \(fragments.count) fragments, wireBytes=\(totalWireBytes)",
|
|
4985
|
+
bridgeLogging: bridgeLogging
|
|
4986
|
+
)
|
|
4987
|
+
return true
|
|
4988
|
+
}
|
|
4989
|
+
|
|
4990
|
+
func sendJson(
|
|
4991
|
+
_ jsonOriginal: [String: Any],
|
|
4992
|
+
wakeUp: Bool = false,
|
|
4993
|
+
requireAck: Bool = true
|
|
4994
|
+
) {
|
|
4995
|
+
sendJson(jsonOriginal, wakeUp: wakeUp, requireAck: requireAck, bridgeLogging: true)
|
|
4881
4996
|
}
|
|
4882
4997
|
|
|
4883
|
-
func sendJson(
|
|
4998
|
+
@discardableResult private func sendJson(
|
|
4999
|
+
_ jsonOriginal: [String: Any],
|
|
5000
|
+
wakeUp: Bool,
|
|
5001
|
+
requireAck: Bool,
|
|
5002
|
+
bridgeLogging: Bool
|
|
5003
|
+
) -> Bool {
|
|
4884
5004
|
do {
|
|
4885
5005
|
var json = jsonOriginal
|
|
4886
5006
|
var messageId: Int64 = -1
|
|
@@ -4904,14 +5024,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4904
5024
|
)
|
|
4905
5025
|
|
|
4906
5026
|
if useBinaryWireProtocol, isNewVersion {
|
|
4907
|
-
sendJsonBinary(
|
|
5027
|
+
return sendJsonBinary(
|
|
4908
5028
|
jsonString: jsonString,
|
|
4909
5029
|
messageId: messageId,
|
|
4910
5030
|
trackingId: trackingId,
|
|
4911
5031
|
wakeUp: wakeUp,
|
|
4912
|
-
requireAck: requireAck
|
|
5032
|
+
requireAck: requireAck,
|
|
5033
|
+
bridgeLogging: bridgeLogging
|
|
4913
5034
|
)
|
|
4914
|
-
return
|
|
4915
5035
|
}
|
|
4916
5036
|
|
|
4917
5037
|
// First check if the message needs chunking
|
|
@@ -4925,17 +5045,20 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4925
5045
|
|
|
4926
5046
|
// Check if chunking is needed
|
|
4927
5047
|
if MessageChunker.needsChunking(testWrappedJson) {
|
|
4928
|
-
|
|
5048
|
+
transportLog("LIVE: Message exceeds threshold, chunking required", bridgeLogging: bridgeLogging)
|
|
4929
5049
|
|
|
4930
5050
|
// Create chunks
|
|
4931
5051
|
let chunks = MessageChunker.createChunks(
|
|
4932
5052
|
originalJson: jsonString, messageId: messageId, wakeUp: wakeUp
|
|
4933
5053
|
)
|
|
4934
5054
|
guard !chunks.isEmpty else {
|
|
4935
|
-
|
|
4936
|
-
|
|
5055
|
+
transportLog(
|
|
5056
|
+
"LIVE: Failed to create BLE chunks within K900 packet limit",
|
|
5057
|
+
bridgeLogging: bridgeLogging
|
|
5058
|
+
)
|
|
5059
|
+
return false
|
|
4937
5060
|
}
|
|
4938
|
-
|
|
5061
|
+
transportLog("LIVE: Sending \(chunks.count) chunks", bridgeLogging: bridgeLogging)
|
|
4939
5062
|
|
|
4940
5063
|
// Send each chunk
|
|
4941
5064
|
for (index, chunk) in chunks.enumerated() {
|
|
@@ -4943,7 +5066,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4943
5066
|
if let chunkStr = String(data: chunkData, encoding: .utf8) {
|
|
4944
5067
|
// Pack each chunk using the normal K900 protocol
|
|
4945
5068
|
let packedData =
|
|
4946
|
-
packJson(
|
|
5069
|
+
packJson(
|
|
5070
|
+
chunkStr,
|
|
5071
|
+
wakeUp: wakeUp && index == 0,
|
|
5072
|
+
bridgeLogging: bridgeLogging
|
|
5073
|
+
) ?? Data() // Only wakeup on first chunk
|
|
4947
5074
|
|
|
4948
5075
|
// Queue the chunk for sending
|
|
4949
5076
|
// Only track ACK for the final chunk (which has the mId)
|
|
@@ -4964,6 +5091,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4964
5091
|
"chunkJsonBytes": chunkStr.data(using: .utf8)?.count,
|
|
4965
5092
|
"messageBytes": jsonData.count,
|
|
4966
5093
|
])
|
|
5094
|
+
guard !packedData.isEmpty else { return false }
|
|
4967
5095
|
queueSend(packedData, id: chunkTrackingId, trace: trace)
|
|
4968
5096
|
|
|
4969
5097
|
// Add small delay between chunks to avoid overwhelming the connection
|
|
@@ -4973,14 +5101,22 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4973
5101
|
}
|
|
4974
5102
|
}
|
|
4975
5103
|
|
|
4976
|
-
|
|
5104
|
+
transportLog("LIVE: All chunks queued for transmission", bridgeLogging: bridgeLogging)
|
|
4977
5105
|
} else {
|
|
4978
5106
|
// Normal single message transmission
|
|
4979
5107
|
if (json["type"] as? String) == "take_photo" {
|
|
4980
|
-
|
|
5108
|
+
transportLog(
|
|
5109
|
+
"LIVE: PHOTO PIPELINE BLE handoff — sendJson -> queueSend take_photo",
|
|
5110
|
+
bridgeLogging: bridgeLogging
|
|
5111
|
+
)
|
|
4981
5112
|
}
|
|
4982
|
-
|
|
4983
|
-
let packedData =
|
|
5113
|
+
transportLog("LIVE: Sending data to glasses: \(jsonString)", bridgeLogging: bridgeLogging)
|
|
5114
|
+
let packedData =
|
|
5115
|
+
packJson(
|
|
5116
|
+
jsonString,
|
|
5117
|
+
wakeUp: wakeUp,
|
|
5118
|
+
bridgeLogging: bridgeLogging
|
|
5119
|
+
) ?? Data()
|
|
4984
5120
|
let trace = createBleWriteTrace(
|
|
4985
5121
|
commandInfo: commandInfo,
|
|
4986
5122
|
chunkId: nil,
|
|
@@ -4994,12 +5130,15 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4994
5130
|
logBleChunkTrace("created", trace, extra: [
|
|
4995
5131
|
"messageBytes": jsonData.count,
|
|
4996
5132
|
])
|
|
5133
|
+
guard !packedData.isEmpty else { return false }
|
|
4997
5134
|
queueSend(packedData, id: trackingId, trace: trace)
|
|
4998
5135
|
}
|
|
5136
|
+
return true
|
|
4999
5137
|
}
|
|
5000
5138
|
} catch {
|
|
5001
|
-
|
|
5139
|
+
transportLog("LIVE: Error creating JSON: \(error)", bridgeLogging: bridgeLogging)
|
|
5002
5140
|
}
|
|
5141
|
+
return false
|
|
5003
5142
|
}
|
|
5004
5143
|
|
|
5005
5144
|
// MARK: - Status Requests
|
|
@@ -5990,7 +6129,11 @@ extension MentraLive {
|
|
|
5990
6129
|
* 1. Wrap with C-field: {"C": jsonData}
|
|
5991
6130
|
* 2. Then pack with BES2700 protocol using little-endian: ## + type + length + {"C": jsonData} + $$
|
|
5992
6131
|
*/
|
|
5993
|
-
private func packJson(
|
|
6132
|
+
private func packJson(
|
|
6133
|
+
_ jsonData: String?,
|
|
6134
|
+
wakeUp: Bool = false,
|
|
6135
|
+
bridgeLogging: Bool = true
|
|
6136
|
+
) -> Data? {
|
|
5994
6137
|
guard let jsonData else { return nil }
|
|
5995
6138
|
|
|
5996
6139
|
do {
|
|
@@ -6013,7 +6156,10 @@ extension MentraLive {
|
|
|
6013
6156
|
)
|
|
6014
6157
|
|
|
6015
6158
|
} catch {
|
|
6016
|
-
|
|
6159
|
+
transportLog(
|
|
6160
|
+
"Error creating JSON wrapper for K900: \(error)",
|
|
6161
|
+
bridgeLogging: bridgeLogging
|
|
6162
|
+
)
|
|
6017
6163
|
return nil
|
|
6018
6164
|
}
|
|
6019
6165
|
}
|
|
@@ -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(
|
|
@@ -376,6 +376,10 @@ public struct VersionInfoResult: CustomStringConvertible {
|
|
|
376
376
|
public let appVersion: String
|
|
377
377
|
public let packageName: String
|
|
378
378
|
public let hotspotOtaVersion: Int
|
|
379
|
+
public let wifiForgetResultVersion: Int?
|
|
380
|
+
public let savedWifiNetworksVersion: Int?
|
|
381
|
+
public let versionInfoType: String?
|
|
382
|
+
public let sid: String?
|
|
379
383
|
|
|
380
384
|
init(status: GlassesStatus) {
|
|
381
385
|
androidVersion = status.androidVersion
|
|
@@ -388,6 +392,10 @@ public struct VersionInfoResult: CustomStringConvertible {
|
|
|
388
392
|
appVersion = status.appVersion
|
|
389
393
|
packageName = status.packageName
|
|
390
394
|
hotspotOtaVersion = status.hotspotOtaVersion
|
|
395
|
+
wifiForgetResultVersion = nil
|
|
396
|
+
savedWifiNetworksVersion = nil
|
|
397
|
+
versionInfoType = nil
|
|
398
|
+
sid = nil
|
|
391
399
|
}
|
|
392
400
|
|
|
393
401
|
init(values: [String: Any]) {
|
|
@@ -404,6 +412,14 @@ public struct VersionInfoResult: CustomStringConvertible {
|
|
|
404
412
|
intValue(values["hotspotOtaVersion"])
|
|
405
413
|
?? intValue(values["hotspot_ota_version"])
|
|
406
414
|
?? 0
|
|
415
|
+
wifiForgetResultVersion =
|
|
416
|
+
intValue(values["wifiForgetResultVersion"])
|
|
417
|
+
?? intValue(values["wifi_forget_result_version"])
|
|
418
|
+
savedWifiNetworksVersion =
|
|
419
|
+
intValue(values["savedWifiNetworksVersion"])
|
|
420
|
+
?? intValue(values["saved_wifi_networks_version"])
|
|
421
|
+
versionInfoType = stringValue(values, "versionInfoType", "version_info_type")
|
|
422
|
+
sid = stringValue(values, "sid")
|
|
407
423
|
}
|
|
408
424
|
|
|
409
425
|
public var dictionary: [String: Any] {
|
|
@@ -425,6 +441,18 @@ public struct VersionInfoResult: CustomStringConvertible {
|
|
|
425
441
|
if !packageName.isEmpty {
|
|
426
442
|
values["packageName"] = packageName
|
|
427
443
|
}
|
|
444
|
+
if let wifiForgetResultVersion {
|
|
445
|
+
values["wifiForgetResultVersion"] = wifiForgetResultVersion
|
|
446
|
+
}
|
|
447
|
+
if let savedWifiNetworksVersion {
|
|
448
|
+
values["savedWifiNetworksVersion"] = savedWifiNetworksVersion
|
|
449
|
+
}
|
|
450
|
+
if let versionInfoType {
|
|
451
|
+
values["versionInfoType"] = versionInfoType
|
|
452
|
+
}
|
|
453
|
+
if let sid {
|
|
454
|
+
values["sid"] = sid
|
|
455
|
+
}
|
|
428
456
|
return values
|
|
429
457
|
}
|
|
430
458
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import CoreFoundation
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Process-scoped native ownership survives BLE reconnects, not app termination.
|
|
5
|
+
enum StreamControllerProbe {
|
|
6
|
+
static let controllerId = UUID().uuidString
|
|
7
|
+
|
|
8
|
+
static func response(_ values: [String: Any]) -> [String: Any]? {
|
|
9
|
+
guard let version = values["protocolVersion"] as? NSNumber,
|
|
10
|
+
CFGetTypeID(version) != CFBooleanGetTypeID(), version.doubleValue == 1,
|
|
11
|
+
values["controllerId"] as? String == controllerId,
|
|
12
|
+
let streamId = values["streamId"] as? String, !streamId.isEmpty,
|
|
13
|
+
let probeId = values["probeId"] as? String, !probeId.isEmpty
|
|
14
|
+
else { return nil }
|
|
15
|
+
return ["type": "stream_controller_response", "protocolVersion": 1,
|
|
16
|
+
"controllerId": controllerId, "streamId": streamId, "probeId": probeId]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -629,6 +629,10 @@ public enum StreamStatus: CustomStringConvertible, Equatable {
|
|
|
629
629
|
public struct StreamStatusEvent: CustomStringConvertible {
|
|
630
630
|
public let status: StreamStatus
|
|
631
631
|
public let stats: StreamLiveStats?
|
|
632
|
+
public private(set) var processSessionId: String?
|
|
633
|
+
public private(set) var revision: Int?
|
|
634
|
+
public private(set) var terminal: Bool?
|
|
635
|
+
public private(set) var errorDetails: String?
|
|
632
636
|
/// True when the glasses will retry the failed publisher themselves
|
|
633
637
|
/// (emitting side lands in PR #3488); absent on older firmware and on
|
|
634
638
|
/// events not parsed from a glasses status map. Carried here instead of
|
|
@@ -644,6 +648,10 @@ public struct StreamStatusEvent: CustomStringConvertible {
|
|
|
644
648
|
status = StreamStatus(values: values)
|
|
645
649
|
stats = StreamLiveStats(values: values["stats"] as? [String: Any])
|
|
646
650
|
willRetry = boolValue(values, "willRetry")
|
|
651
|
+
processSessionId = stringValue(values, "sid")
|
|
652
|
+
revision = optionalIntValue(values, "revision")
|
|
653
|
+
terminal = boolValue(values, "terminal")
|
|
654
|
+
errorDetails = stringValue(values, "errorDetails")
|
|
647
655
|
}
|
|
648
656
|
|
|
649
657
|
public var state: StreamState {
|
|
@@ -661,6 +669,10 @@ public struct StreamStatusEvent: CustomStringConvertible {
|
|
|
661
669
|
public var values: [String: Any] {
|
|
662
670
|
var values = status.values
|
|
663
671
|
values["type"] = "stream_status"
|
|
672
|
+
if let processSessionId { values["sid"] = processSessionId }
|
|
673
|
+
if let revision { values["revision"] = revision }
|
|
674
|
+
if let terminal { values["terminal"] = terminal }
|
|
675
|
+
if let errorDetails { values["errorDetails"] = errorDetails }
|
|
664
676
|
if let stats {
|
|
665
677
|
values["stats"] = stats.values
|
|
666
678
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Observes glasses-owned streaming; delivery gaps never imply publisher failure.
|
|
4
|
+
final class StreamSessionState {
|
|
5
|
+
private(set) var processSessionId: String?
|
|
6
|
+
private(set) var currentStreamId: String?
|
|
7
|
+
private(set) var supported = false
|
|
8
|
+
private var revision: Int = -1
|
|
9
|
+
|
|
10
|
+
func ready(sessionId: String?, controlVersion: Int?) {
|
|
11
|
+
supported = controlVersion == 1 && !(sessionId ?? "").isEmpty
|
|
12
|
+
if processSessionId != sessionId {
|
|
13
|
+
currentStreamId = nil
|
|
14
|
+
revision = -1
|
|
15
|
+
}
|
|
16
|
+
processSessionId = sessionId
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func accept(_ values: [String: Any]) -> Bool {
|
|
20
|
+
guard supported,
|
|
21
|
+
let sid = values["sid"] as? String, sid == processSessionId,
|
|
22
|
+
let incomingRevision = values["revision"] as? Int,
|
|
23
|
+
incomingRevision >= revision
|
|
24
|
+
else { return false }
|
|
25
|
+
revision = incomingRevision
|
|
26
|
+
currentStreamId = values["terminal"] as? Bool == true ? nil : values["streamId"] as? String
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class StreamControllerProbeTests: XCTestCase {
|
|
5
|
+
private func probe() -> [String: Any] {
|
|
6
|
+
["protocolVersion": 1, "controllerId": StreamControllerProbe.controllerId,
|
|
7
|
+
"streamId": "stream", "probeId": "nonce"]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
func testAnswersCurrentProcessChallengeWithoutJsOrTimer() {
|
|
11
|
+
let response = StreamControllerProbe.response(probe())
|
|
12
|
+
XCTAssertEqual(response?["type"] as? String, "stream_controller_response")
|
|
13
|
+
XCTAssertEqual(response?["probeId"] as? String, "nonce")
|
|
14
|
+
XCTAssertEqual(response?["streamId"] as? String, "stream")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func testRejectsPreviousProcessAndMalformedChallenges() {
|
|
18
|
+
for (key, value) in [("controllerId", "previous-process" as Any),
|
|
19
|
+
("protocolVersion", 2), ("protocolVersion", true),
|
|
20
|
+
("probeId", ""), ("streamId", "")]
|
|
21
|
+
{
|
|
22
|
+
var input = probe()
|
|
23
|
+
input[key] = value
|
|
24
|
+
XCTAssertNil(StreamControllerProbe.response(input))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|