@mentra/bluetooth-sdk 0.1.17 → 0.1.19
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 +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +52 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +119 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +22 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +457 -31
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +361 -67
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +7501 -3704
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +134 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleWireProtocol.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +4 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900LengthCodec.java +115 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +103 -68
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +93 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +111 -11
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +7 -9
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +17 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +158 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
- package/build/BluetoothSdk.types.d.ts +12 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +2 -0
- package/build/types/index.js.map +1 -0
- package/ios/LocalPhotoUploadServer.swift +78 -0
- package/ios/MentraPhotoReceiverModule.swift +10 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +63 -0
- package/ios/Source/DeviceManager.swift +134 -29
- package/ios/Source/DeviceStore.swift +0 -5
- package/ios/Source/MentraBluetoothSDK.swift +26 -23
- package/ios/Source/OtaManifest.swift +1 -1
- package/ios/Source/internal/BleTraceLogger.swift +192 -0
- package/ios/Source/sgcs/G2.swift +518 -11
- package/ios/Source/sgcs/MentraLive.swift +607 -39
- package/ios/Source/sgcs/MentraNex.swift +416 -32
- package/ios/Source/sgcs/SGCManager.swift +155 -0
- package/ios/Source/sgcs/mentraos_ble.pb.swift +726 -122
- package/ios/Source/status/DeviceStatus.swift +0 -8
- package/ios/Source/utils/BleJsonCompact.swift +395 -0
- package/ios/Source/utils/BleWireProtocol.swift +92 -0
- package/ios/Source/utils/MessageChunkReassembler.swift +82 -0
- package/ios/Source/utils/MessageChunker.swift +73 -0
- package/package.json +14 -7
- package/src/BluetoothSdk.types.ts +13 -1
- package/src/types/index.ts +7 -0
|
@@ -118,7 +118,10 @@ class BlePhotoUploadService {
|
|
|
118
118
|
"\(TAG): Decoded image to bitmap: \(Int(image.size.width))x\(Int(image.size.height))"
|
|
119
119
|
)
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
// 1.0: the source already went through a lossy AVIF pass on the glasses,
|
|
122
|
+
// so this re-encode must not compound the loss. Phone CPU and upload
|
|
123
|
+
// bandwidth are cheap relative to what was paid to get the bytes over BLE.
|
|
124
|
+
guard var jpegData = image.jpegData(compressionQuality: 1.0) else {
|
|
122
125
|
throw NSError(
|
|
123
126
|
domain: "BlePhotoUpload",
|
|
124
127
|
code: -2,
|
|
@@ -497,6 +500,7 @@ private enum K900ProtocolUtils {
|
|
|
497
500
|
static let CMD_START_CODE: [UInt8] = [0x23, 0x23] // ##
|
|
498
501
|
static let CMD_END_CODE: [UInt8] = [0x24, 0x24] // $$
|
|
499
502
|
static let CMD_TYPE_STRING: UInt8 = 0x30 // String/JSON type
|
|
503
|
+
static let CMD_TYPE_BINARY_MSG: UInt8 = 0x40
|
|
500
504
|
|
|
501
505
|
// JSON Field constants
|
|
502
506
|
static let FIELD_C = "C" // Command/Content field
|
|
@@ -1134,9 +1138,13 @@ extension MentraLive: CBPeripheralDelegate {
|
|
|
1134
1138
|
|
|
1135
1139
|
nonisolated func peripheral(_: CBPeripheral, didWriteValueFor _: CBCharacteristic, error: Error?) {
|
|
1136
1140
|
let errorDescription = error?.localizedDescription
|
|
1137
|
-
DispatchQueue.main.async {
|
|
1141
|
+
DispatchQueue.main.async { [weak self] in
|
|
1138
1142
|
if let errorDescription {
|
|
1139
1143
|
Bridge.log("LIVE: Error writing characteristic: \(errorDescription)")
|
|
1144
|
+
self?.logBleWriteTrace("write_callback", self?.pending?.trace, extra: [
|
|
1145
|
+
"success": false,
|
|
1146
|
+
"errorMessage": errorDescription,
|
|
1147
|
+
])
|
|
1140
1148
|
} else {
|
|
1141
1149
|
Bridge.log("LIVE: Characteristic write successful")
|
|
1142
1150
|
}
|
|
@@ -1214,6 +1222,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1214
1222
|
// or stale lastBesOtaProgress on the next OTA).
|
|
1215
1223
|
if state == ConnTypes.DISCONNECTED {
|
|
1216
1224
|
incomingChunkReassembler.clear()
|
|
1225
|
+
peerWireProtocolVersion = 0
|
|
1226
|
+
useBinaryWireProtocol = false
|
|
1227
|
+
wireHandshakeQueued = false
|
|
1228
|
+
peerK900Le = false
|
|
1229
|
+
peerWireCapsBinary = false
|
|
1230
|
+
BleJsonCompact.resetSession()
|
|
1217
1231
|
stopSignalStrengthPolling()
|
|
1218
1232
|
DeviceStore.shared.apply("glasses", "signalStrength", -1)
|
|
1219
1233
|
DeviceStore.shared.apply("glasses", "signalStrengthUpdatedAt", 0)
|
|
@@ -1358,6 +1372,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1358
1372
|
private let SIGNAL_STRENGTH_READ_INTERVAL_MS: TimeInterval = 10.0
|
|
1359
1373
|
private let MIN_SEND_DELAY_MS: UInt64 = 160_000_000 // 160ms in nanoseconds
|
|
1360
1374
|
private let READINESS_CHECK_INTERVAL_MS: TimeInterval = 2.5 // 2.5 seconds
|
|
1375
|
+
private let SIGNIFICANT_BLE_TRACE_DELAY_MS: TimeInterval = 250
|
|
1376
|
+
private let SIGNIFICANT_BLE_TRACE_QUEUE_SIZE = 5
|
|
1361
1377
|
|
|
1362
1378
|
/// Device Settings Keys
|
|
1363
1379
|
private let PREFS_DEVICE_NAME = "MentraLiveLastConnectedDeviceName"
|
|
@@ -1374,7 +1390,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1374
1390
|
private var connectedPeripheral: CBPeripheral?
|
|
1375
1391
|
private var txCharacteristic: CBCharacteristic?
|
|
1376
1392
|
private var rxCharacteristic: CBCharacteristic?
|
|
1377
|
-
private let bes2700MtuLimit =
|
|
1393
|
+
private let bes2700MtuLimit = 509
|
|
1378
1394
|
private var currentMtu: Int = 23 // Default BLE MTU
|
|
1379
1395
|
|
|
1380
1396
|
// State Tracking
|
|
@@ -1383,8 +1399,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1383
1399
|
private var isKilled = false
|
|
1384
1400
|
private var reconnectAttempts = 0
|
|
1385
1401
|
private var isNewVersion = false
|
|
1402
|
+
private var peerWireProtocolVersion = 0
|
|
1403
|
+
private var useBinaryWireProtocol = false
|
|
1404
|
+
private var wireHandshakeQueued = false
|
|
1405
|
+
// Negotiated K900 STRING length endianness for the phone<->glasses BLE link. Defaults to legacy
|
|
1406
|
+
// big-endian; upgraded to little-endian only when the glasses advertise wire_caps.k900_le (or a
|
|
1407
|
+
// v2 binary handshake succeeds, which implies wire-v2 LE).
|
|
1408
|
+
private var peerK900Le = false
|
|
1409
|
+
private var peerWireCapsBinary = false
|
|
1386
1410
|
private var globalMessageId = 0
|
|
1387
1411
|
private var lastReceivedMessageId = 0
|
|
1412
|
+
private var bleWriteTraceSequence = 0
|
|
1388
1413
|
|
|
1389
1414
|
private var fullyBooted: Bool {
|
|
1390
1415
|
get { DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false }
|
|
@@ -1506,7 +1531,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1506
1531
|
Bridge.log("Found already-connected peripheral: \(peripheral.name ?? "Unknown")")
|
|
1507
1532
|
if let name = peripheral.name,
|
|
1508
1533
|
name == "Xy_A" || name.hasPrefix("XyBLE_") || name.hasPrefix("MENTRA_LIVE_BLE")
|
|
1509
|
-
|| name.hasPrefix("MENTRA_LIVE_BT")
|
|
1534
|
+
|| name.hasPrefix("MENTRA_LIVE_BT") || name.lowercased().hasPrefix("mentra_live")
|
|
1510
1535
|
{
|
|
1511
1536
|
Bridge.log("Found already-connected peripheral: \(name)")
|
|
1512
1537
|
discoveredPeripherals[name] = peripheral
|
|
@@ -1741,15 +1766,40 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1741
1766
|
// MARK: - Command Queue
|
|
1742
1767
|
|
|
1743
1768
|
class PendingMessage {
|
|
1744
|
-
init(data: Data, id: String, retries: Int) {
|
|
1769
|
+
init(data: Data, id: String, retries: Int, trace: BleWriteTrace? = nil) {
|
|
1745
1770
|
self.data = data
|
|
1746
1771
|
self.id = id
|
|
1747
1772
|
self.retries = retries
|
|
1773
|
+
self.trace = trace
|
|
1748
1774
|
}
|
|
1749
1775
|
|
|
1750
1776
|
let data: Data
|
|
1751
1777
|
let retries: Int
|
|
1752
1778
|
let id: String
|
|
1779
|
+
let trace: BleWriteTrace?
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
struct OutgoingBleCommandTraceInfo {
|
|
1783
|
+
let commandType: String
|
|
1784
|
+
let requestId: String?
|
|
1785
|
+
let appId: String?
|
|
1786
|
+
let messageId: Int64?
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
struct BleWriteTrace {
|
|
1790
|
+
let sequence: Int
|
|
1791
|
+
let commandType: String
|
|
1792
|
+
let requestId: String?
|
|
1793
|
+
let appId: String?
|
|
1794
|
+
let messageId: Int64?
|
|
1795
|
+
let chunkId: String?
|
|
1796
|
+
let chunkIndex: Int?
|
|
1797
|
+
let totalChunks: Int?
|
|
1798
|
+
let payloadBytes: Int?
|
|
1799
|
+
let packedBytes: Int
|
|
1800
|
+
let wakeup: Bool
|
|
1801
|
+
let chunked: Bool
|
|
1802
|
+
let queuedAtMs: TimeInterval
|
|
1753
1803
|
}
|
|
1754
1804
|
|
|
1755
1805
|
private var pending: PendingMessage?
|
|
@@ -1758,12 +1808,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1758
1808
|
actor CommandQueue {
|
|
1759
1809
|
private var commands: [PendingMessage] = []
|
|
1760
1810
|
|
|
1761
|
-
func enqueue(_ command: PendingMessage) {
|
|
1811
|
+
func enqueue(_ command: PendingMessage) -> Int {
|
|
1762
1812
|
commands.append(command)
|
|
1813
|
+
return commands.count
|
|
1763
1814
|
}
|
|
1764
1815
|
|
|
1765
|
-
func pushToFront(_ command: PendingMessage) {
|
|
1816
|
+
func pushToFront(_ command: PendingMessage) -> Int {
|
|
1766
1817
|
commands.insert(command, at: 0)
|
|
1818
|
+
return commands.count
|
|
1767
1819
|
}
|
|
1768
1820
|
|
|
1769
1821
|
func dequeue() -> PendingMessage? {
|
|
@@ -1791,18 +1843,28 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1791
1843
|
guard let peripheral = connectedPeripheral,
|
|
1792
1844
|
let txChar = txCharacteristic
|
|
1793
1845
|
else {
|
|
1846
|
+
logBleWriteTrace("write_dropped", message.trace, extra: [
|
|
1847
|
+
"errorMessage": "missing peripheral or TX characteristic",
|
|
1848
|
+
])
|
|
1794
1849
|
return
|
|
1795
1850
|
}
|
|
1796
1851
|
|
|
1797
1852
|
// Enforce rate limiting
|
|
1798
1853
|
let currentTime = Date().timeIntervalSince1970 * 1000
|
|
1799
1854
|
let timeSinceLastSend = currentTime - lastSendTimeMs
|
|
1855
|
+
let queueDelayMs = message.trace.map { currentTime - $0.queuedAtMs }
|
|
1800
1856
|
|
|
1801
1857
|
try? await Task.sleep(nanoseconds: UInt64(1_000_000))
|
|
1802
1858
|
lastSendTimeMs = Date().timeIntervalSince1970 * 1000
|
|
1803
1859
|
|
|
1804
1860
|
// Send the data
|
|
1805
1861
|
peripheral.writeValue(message.data, for: txChar, type: .withResponse)
|
|
1862
|
+
logBleWriteTrace("write_requested", message.trace, extra: [
|
|
1863
|
+
"queueDelayMs": queueDelayMs,
|
|
1864
|
+
"timeSinceLastSendMs": timeSinceLastSend,
|
|
1865
|
+
"currentMtu": currentMtu,
|
|
1866
|
+
"writeType": "withResponse",
|
|
1867
|
+
])
|
|
1806
1868
|
|
|
1807
1869
|
// don't do the retry system on the old glasses versions
|
|
1808
1870
|
if !isNewVersion {
|
|
@@ -1844,12 +1906,19 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1844
1906
|
let retryMessage = PendingMessage(
|
|
1845
1907
|
data: pendingMessage.data,
|
|
1846
1908
|
id: pendingMessage.id,
|
|
1847
|
-
retries: pendingMessage.retries + 1
|
|
1909
|
+
retries: pendingMessage.retries + 1,
|
|
1910
|
+
trace: pendingMessage.trace
|
|
1848
1911
|
)
|
|
1849
1912
|
|
|
1850
1913
|
// Push to front of queue for immediate retry
|
|
1851
1914
|
Task {
|
|
1852
|
-
await self.commandQueue.pushToFront(retryMessage)
|
|
1915
|
+
let queueSize = await self.commandQueue.pushToFront(retryMessage)
|
|
1916
|
+
await MainActor.run {
|
|
1917
|
+
self.logBleWriteTrace("retry_queued", retryMessage.trace, extra: [
|
|
1918
|
+
"retryDelayMs": 0,
|
|
1919
|
+
"queueSizeAfterAdd": queueSize,
|
|
1920
|
+
])
|
|
1921
|
+
}
|
|
1853
1922
|
}
|
|
1854
1923
|
|
|
1855
1924
|
Bridge.log(
|
|
@@ -2055,17 +2124,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2055
2124
|
return // Exit after processing file packet
|
|
2056
2125
|
}
|
|
2057
2126
|
|
|
2058
|
-
|
|
2127
|
+
if commandType == K900ProtocolUtils.CMD_TYPE_BINARY_MSG {
|
|
2128
|
+
processBinaryWireFrame(data)
|
|
2129
|
+
return
|
|
2130
|
+
}
|
|
2059
2131
|
|
|
2060
|
-
//
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
} else {
|
|
2067
|
-
// Standard device - little-endian
|
|
2068
|
-
payloadLength = (Int(bytes[4]) << 8) | Int(bytes[3])
|
|
2132
|
+
// Auto-detect the length endianness so we parse both legacy big-endian and wire-v2
|
|
2133
|
+
// little-endian frames, and learn the glasses' endianness for future outbound frames.
|
|
2134
|
+
let detected = k900DetectStringLength(bytes)
|
|
2135
|
+
let payloadLength = detected?.length ?? (Int(bytes[3]) | (Int(bytes[4]) << 8))
|
|
2136
|
+
if let detected {
|
|
2137
|
+
peerK900Le = detected.isLe
|
|
2069
2138
|
}
|
|
2070
2139
|
|
|
2071
2140
|
// Bridge.log(
|
|
@@ -2099,9 +2168,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2099
2168
|
}
|
|
2100
2169
|
}
|
|
2101
2170
|
|
|
2102
|
-
private func processJsonObject(_
|
|
2171
|
+
private func processJsonObject(_ incoming: [String: Any]) {
|
|
2172
|
+
guard let json = expandCompactWireJson(incoming) else {
|
|
2173
|
+
Bridge.log("LIVE: Rejected unsupported compact wire form")
|
|
2174
|
+
return
|
|
2175
|
+
}
|
|
2103
2176
|
// Log ALL incoming JSON objects for debugging
|
|
2104
2177
|
// Bridge.log("LIVE: DEBUG: processJsonObject: \(json)")
|
|
2178
|
+
BleTraceLogger.logJson(direction: "glasses_to_phone", layer: "sdk_ble_event", payload: json)
|
|
2105
2179
|
|
|
2106
2180
|
if MessageChunker.isChunkedMessage(json) {
|
|
2107
2181
|
processChunkedJsonObject(json)
|
|
@@ -2147,12 +2221,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2147
2221
|
|
|
2148
2222
|
switch type {
|
|
2149
2223
|
case "glasses_ready":
|
|
2224
|
+
parsePeerWireCaps(json)
|
|
2150
2225
|
handleGlassesReady()
|
|
2151
2226
|
|
|
2152
2227
|
case "battery_status":
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2228
|
+
// Percent only (the glasses send it as "percent"; the old "level" read never
|
|
2229
|
+
// matched). battery_status derives from hm_batv, which carries no charge bit —
|
|
2230
|
+
// older glasses fabricate `charging` from a voltage threshold. Charging state
|
|
2231
|
+
// comes exclusively from the PMU charg bit in the sr_hrt heartbeat.
|
|
2232
|
+
let level = json["percent"] as? Int ?? json["level"] as? Int ?? batteryLevel
|
|
2233
|
+
updateBatteryStatus(level: level, isCharging: charging)
|
|
2156
2234
|
|
|
2157
2235
|
case "voice_activity_detection_status":
|
|
2158
2236
|
let enabled = json["voiceActivityDetectionEnabled"] as? Bool
|
|
@@ -2167,6 +2245,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2167
2245
|
let connected = json["connected"] as? Bool ?? false
|
|
2168
2246
|
let ssid = json["ssid"] as? String ?? ""
|
|
2169
2247
|
let ip = json["local_ip"] as? String ?? ""
|
|
2248
|
+
// Provisioning failure reason (e.g. connect_timeout). Sticky: routine
|
|
2249
|
+
// error-less status updates (link-state debounce, request_wifi_status)
|
|
2250
|
+
// must not clear a failure nothing recovered from — only a newer error
|
|
2251
|
+
// or a successful connection overwrites it.
|
|
2252
|
+
let wifiError = json["error"] as? String ?? ""
|
|
2253
|
+
if !wifiError.isEmpty {
|
|
2254
|
+
Bridge.log("LIVE: 🌐 WiFi provisioning error from glasses: \(wifiError)")
|
|
2255
|
+
DeviceStore.shared.apply("glasses", "wifiError", wifiError)
|
|
2256
|
+
} else if connected {
|
|
2257
|
+
DeviceStore.shared.apply("glasses", "wifiError", "")
|
|
2258
|
+
}
|
|
2170
2259
|
updateWifiStatus(connected: connected, ssid: ssid, ip: ip)
|
|
2171
2260
|
|
|
2172
2261
|
case "hotspot_status_update":
|
|
@@ -2411,6 +2500,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2411
2500
|
if let buildNumber = fields["build_number"] as? String {
|
|
2412
2501
|
isNewVersion = (Int(buildNumber) ?? 0) >= 5
|
|
2413
2502
|
DeviceStore.shared.apply("glasses", "buildNumber", buildNumber)
|
|
2503
|
+
parsePeerWireCaps(json)
|
|
2504
|
+
maybeSendWireHandshake()
|
|
2414
2505
|
}
|
|
2415
2506
|
if let deviceModel = fields["device_model"] as? String {
|
|
2416
2507
|
DeviceStore.shared.apply("glasses", "deviceModel", deviceModel)
|
|
@@ -2543,10 +2634,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2543
2634
|
if let bodyObj = json["B"] as? [String: Any] {
|
|
2544
2635
|
let readyResponse = bodyObj["ready"] as? Int ?? 0
|
|
2545
2636
|
|
|
2546
|
-
// Extract battery info from heartbeat
|
|
2637
|
+
// Extract battery info from heartbeat. charg is the PMU charging bit — the
|
|
2638
|
+
// only truthful charging source in the protocol. Old firmware omits it;
|
|
2639
|
+
// keep the last known state then instead of defaulting to not-charging.
|
|
2547
2640
|
let percentage = bodyObj["pt"] as? Int ?? 0
|
|
2548
2641
|
let voltage = bodyObj["vt"] as? Int ?? 0
|
|
2549
|
-
let
|
|
2642
|
+
let chargBit = bodyObj["charg"] as? Int
|
|
2643
|
+
let charging = chargBit != nil ? (chargBit == 1) : self.charging
|
|
2550
2644
|
|
|
2551
2645
|
// SOC is still booting
|
|
2552
2646
|
if readyResponse == 0 {
|
|
@@ -2594,12 +2688,15 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2594
2688
|
let percentage = body["pt"] as? Int
|
|
2595
2689
|
{
|
|
2596
2690
|
let voltageVolts = Double(voltage) / 1000.0
|
|
2597
|
-
let isCharging = voltage > 4000
|
|
2598
2691
|
|
|
2599
2692
|
Bridge.log(
|
|
2600
2693
|
"🔋 K900 Battery Status - Voltage: \(voltageVolts)V, Level: \(percentage)%"
|
|
2601
2694
|
)
|
|
2602
|
-
|
|
2695
|
+
// Percent only. sr_batv carries just voltage+percent; inferring charging
|
|
2696
|
+
// from voltage (>4.0V) reads "not charging" for most of a genuinely-charging
|
|
2697
|
+
// pack's range. Charging state comes exclusively from the PMU charg bit in
|
|
2698
|
+
// the sr_hrt heartbeat.
|
|
2699
|
+
updateBatteryStatus(level: percentage, isCharging: charging)
|
|
2603
2700
|
}
|
|
2604
2701
|
|
|
2605
2702
|
case "sr_getvol":
|
|
@@ -3005,6 +3102,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3005
3102
|
DeviceStore.shared.apply("glasses", "firmwareVersion", firmwareVersion)
|
|
3006
3103
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
3007
3104
|
isNewVersion = (Int(buildNumber) ?? 0) >= 5
|
|
3105
|
+
maybeSendWireHandshake()
|
|
3008
3106
|
DeviceStore.shared.apply("glasses", "deviceModel", deviceModel)
|
|
3009
3107
|
DeviceStore.shared.apply("glasses", "androidVersion", androidVersion)
|
|
3010
3108
|
|
|
@@ -3514,7 +3612,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3514
3612
|
let basePath = components.path.hasSuffix("/")
|
|
3515
3613
|
? String(components.path.dropLast())
|
|
3516
3614
|
: components.path
|
|
3517
|
-
components.path = basePath + "/api/
|
|
3615
|
+
components.path = basePath + "/api/client/reports/\(relay.incidentId)/artifacts"
|
|
3518
3616
|
guard let url = components.url else {
|
|
3519
3617
|
sendTransferCompleteConfirmation(fileName: fileName, success: false)
|
|
3520
3618
|
if let existing = bleIncidentLogRelays[relay.fileBaseKey] {
|
|
@@ -3651,11 +3749,370 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3651
3749
|
// MARK: - Sending Data
|
|
3652
3750
|
|
|
3653
3751
|
func queueSend(_ data: Data, id: String) {
|
|
3752
|
+
queueSend(data, id: id, trace: nil)
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
func queueSend(_ data: Data, id: String, trace: BleWriteTrace?) {
|
|
3756
|
+
let significantQueueSize = SIGNIFICANT_BLE_TRACE_QUEUE_SIZE
|
|
3654
3757
|
Task {
|
|
3655
|
-
await commandQueue.enqueue(PendingMessage(data: data, id: id, retries: 0))
|
|
3758
|
+
let queueSize = await commandQueue.enqueue(PendingMessage(data: data, id: id, retries: 0, trace: trace))
|
|
3759
|
+
guard trace != nil, queueSize >= significantQueueSize else {
|
|
3760
|
+
return
|
|
3761
|
+
}
|
|
3762
|
+
await MainActor.run {
|
|
3763
|
+
self.logBleChunkTrace("queued", trace, extra: [
|
|
3764
|
+
"queueSizeAfterAdd": queueSize,
|
|
3765
|
+
])
|
|
3766
|
+
}
|
|
3656
3767
|
}
|
|
3657
3768
|
}
|
|
3658
3769
|
|
|
3770
|
+
private func outgoingBleCommandTraceInfo(_ json: [String: Any]) -> OutgoingBleCommandTraceInfo {
|
|
3771
|
+
OutgoingBleCommandTraceInfo(
|
|
3772
|
+
commandType: nonBlankString(json["type"]) ?? "unknown",
|
|
3773
|
+
requestId: nonBlankString(json["requestId"]),
|
|
3774
|
+
appId: nonBlankString(json["appId"]),
|
|
3775
|
+
messageId: traceInt64(json["mId"])
|
|
3776
|
+
)
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
private func createBleWriteTrace(
|
|
3780
|
+
commandInfo: OutgoingBleCommandTraceInfo,
|
|
3781
|
+
chunkId: String?,
|
|
3782
|
+
chunkIndex: Int?,
|
|
3783
|
+
totalChunks: Int?,
|
|
3784
|
+
payloadBytes: Int?,
|
|
3785
|
+
packedBytes: Int,
|
|
3786
|
+
wakeup: Bool,
|
|
3787
|
+
chunked: Bool
|
|
3788
|
+
) -> BleWriteTrace {
|
|
3789
|
+
let sequence = bleWriteTraceSequence
|
|
3790
|
+
bleWriteTraceSequence += 1
|
|
3791
|
+
return BleWriteTrace(
|
|
3792
|
+
sequence: sequence,
|
|
3793
|
+
commandType: commandInfo.commandType,
|
|
3794
|
+
requestId: commandInfo.requestId,
|
|
3795
|
+
appId: commandInfo.appId,
|
|
3796
|
+
messageId: commandInfo.messageId,
|
|
3797
|
+
chunkId: chunkId,
|
|
3798
|
+
chunkIndex: chunkIndex,
|
|
3799
|
+
totalChunks: totalChunks,
|
|
3800
|
+
payloadBytes: payloadBytes,
|
|
3801
|
+
packedBytes: packedBytes,
|
|
3802
|
+
wakeup: wakeup,
|
|
3803
|
+
chunked: chunked,
|
|
3804
|
+
queuedAtMs: Date().timeIntervalSince1970 * 1000
|
|
3805
|
+
)
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
private func logBleChunkTrace(
|
|
3809
|
+
_ stage: String,
|
|
3810
|
+
_ trace: BleWriteTrace?,
|
|
3811
|
+
extra: [String: Any?] = [:]
|
|
3812
|
+
) {
|
|
3813
|
+
logBleTrace(layer: "sdk_ble_chunk", stage: stage, trace: trace, extra: extra)
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
private func logBleWriteTrace(
|
|
3817
|
+
_ stage: String,
|
|
3818
|
+
_ trace: BleWriteTrace?,
|
|
3819
|
+
extra: [String: Any?] = [:]
|
|
3820
|
+
) {
|
|
3821
|
+
logBleTrace(layer: "sdk_ble_write", stage: stage, trace: trace, extra: extra)
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
private func logBleTrace(
|
|
3825
|
+
layer: String,
|
|
3826
|
+
stage: String,
|
|
3827
|
+
trace: BleWriteTrace?,
|
|
3828
|
+
extra: [String: Any?] = [:]
|
|
3829
|
+
) {
|
|
3830
|
+
guard let trace,
|
|
3831
|
+
let warningReason = bleTraceWarningReason(stage: stage, extra: extra)
|
|
3832
|
+
else {
|
|
3833
|
+
return
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
var payload: [String: Any] = [
|
|
3837
|
+
"level": "warning",
|
|
3838
|
+
"warningReason": warningReason,
|
|
3839
|
+
"stage": stage,
|
|
3840
|
+
"sequence": trace.sequence,
|
|
3841
|
+
"commandType": trace.commandType,
|
|
3842
|
+
"packedBytes": trace.packedBytes,
|
|
3843
|
+
"wakeup": trace.wakeup,
|
|
3844
|
+
"chunked": trace.chunked,
|
|
3845
|
+
"queuedAtMs": trace.queuedAtMs,
|
|
3846
|
+
]
|
|
3847
|
+
if let requestId = trace.requestId { payload["requestId"] = requestId }
|
|
3848
|
+
if let appId = trace.appId { payload["appId"] = appId }
|
|
3849
|
+
if let messageId = trace.messageId { payload["messageId"] = messageId }
|
|
3850
|
+
if let chunkId = trace.chunkId { payload["chunkId"] = chunkId }
|
|
3851
|
+
if let chunkIndex = trace.chunkIndex {
|
|
3852
|
+
payload["chunkIndex"] = chunkIndex
|
|
3853
|
+
payload["chunkNumber"] = chunkIndex + 1
|
|
3854
|
+
}
|
|
3855
|
+
if let totalChunks = trace.totalChunks { payload["totalChunks"] = totalChunks }
|
|
3856
|
+
if let payloadBytes = trace.payloadBytes { payload["payloadBytes"] = payloadBytes }
|
|
3857
|
+
for (key, value) in extra {
|
|
3858
|
+
if let value {
|
|
3859
|
+
payload[key] = value
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
BleTraceLogger.logMap(direction: "phone_to_glasses", layer: layer, type: trace.commandType, payload: payload)
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
private func bleTraceWarningReason(stage: String, extra: [String: Any?]) -> String? {
|
|
3867
|
+
if extraValue(extra, "errorClass") != nil || extraValue(extra, "errorMessage") != nil {
|
|
3868
|
+
return "ble_write_error"
|
|
3869
|
+
}
|
|
3870
|
+
if let success = extraValue(extra, "success") as? Bool, !success {
|
|
3871
|
+
return "ble_write_failed"
|
|
3872
|
+
}
|
|
3873
|
+
if let writeAccepted = extraValue(extra, "writeAccepted") as? Bool, !writeAccepted {
|
|
3874
|
+
return "ble_write_rejected"
|
|
3875
|
+
}
|
|
3876
|
+
if traceDouble(extraValue(extra, "queueDelayMs")).map({ $0 >= SIGNIFICANT_BLE_TRACE_DELAY_MS }) == true {
|
|
3877
|
+
return "queue_delay"
|
|
3878
|
+
}
|
|
3879
|
+
if traceDouble(extraValue(extra, "callbackDelayMs")).map({ $0 >= SIGNIFICANT_BLE_TRACE_DELAY_MS }) == true {
|
|
3880
|
+
return "write_callback_delay"
|
|
3881
|
+
}
|
|
3882
|
+
if traceDouble(extraValue(extra, "remainingDelayMs")).map({ $0 >= SIGNIFICANT_BLE_TRACE_DELAY_MS }) == true {
|
|
3883
|
+
return "rate_limit_delay"
|
|
3884
|
+
}
|
|
3885
|
+
if traceDouble(extraValue(extra, "nextProcessDelayMs")).map({ $0 >= SIGNIFICANT_BLE_TRACE_DELAY_MS }) == true {
|
|
3886
|
+
return "next_process_delay"
|
|
3887
|
+
}
|
|
3888
|
+
if extraValue(extra, "retryDelayMs") != nil {
|
|
3889
|
+
return "write_retry"
|
|
3890
|
+
}
|
|
3891
|
+
if stage == "queued",
|
|
3892
|
+
traceInt(extraValue(extra, "queueSizeAfterAdd")).map({ $0 >= SIGNIFICANT_BLE_TRACE_QUEUE_SIZE }) == true
|
|
3893
|
+
{
|
|
3894
|
+
return "queue_depth"
|
|
3895
|
+
}
|
|
3896
|
+
return nil
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
private func extraValue(_ extra: [String: Any?], _ key: String) -> Any? {
|
|
3900
|
+
extra[key] ?? nil
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
private func nonBlankString(_ value: Any?) -> String? {
|
|
3904
|
+
guard let value else { return nil }
|
|
3905
|
+
let string: String
|
|
3906
|
+
if let value = value as? String {
|
|
3907
|
+
string = value
|
|
3908
|
+
} else {
|
|
3909
|
+
string = String(describing: value)
|
|
3910
|
+
}
|
|
3911
|
+
return string.isEmpty ? nil : string
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
private func traceInt64(_ value: Any?) -> Int64? {
|
|
3915
|
+
guard let value else { return nil }
|
|
3916
|
+
switch value {
|
|
3917
|
+
case let value as Int64:
|
|
3918
|
+
return value
|
|
3919
|
+
case let value as Int:
|
|
3920
|
+
return Int64(value)
|
|
3921
|
+
case let value as NSNumber:
|
|
3922
|
+
return value.int64Value
|
|
3923
|
+
case let value as String:
|
|
3924
|
+
return Int64(value)
|
|
3925
|
+
default:
|
|
3926
|
+
return nil
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
private func traceInt(_ value: Any?) -> Int? {
|
|
3931
|
+
guard let value else { return nil }
|
|
3932
|
+
switch value {
|
|
3933
|
+
case let value as Int:
|
|
3934
|
+
return value
|
|
3935
|
+
case let value as NSNumber:
|
|
3936
|
+
return value.intValue
|
|
3937
|
+
case let value as String:
|
|
3938
|
+
return Int(value)
|
|
3939
|
+
default:
|
|
3940
|
+
return nil
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
private func traceDouble(_ value: Any?) -> Double? {
|
|
3945
|
+
guard let value else { return nil }
|
|
3946
|
+
switch value {
|
|
3947
|
+
case let value as Double:
|
|
3948
|
+
return value
|
|
3949
|
+
case let value as Int:
|
|
3950
|
+
return Double(value)
|
|
3951
|
+
case let value as NSNumber:
|
|
3952
|
+
return value.doubleValue
|
|
3953
|
+
case let value as String:
|
|
3954
|
+
return Double(value)
|
|
3955
|
+
default:
|
|
3956
|
+
return nil
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
private func maybeSendWireHandshake() {
|
|
3961
|
+
// Only attempt the v2 binary handshake once the glasses have advertised binary support via
|
|
3962
|
+
// wire_caps. Older builds that report new version but lack wire_caps stay on the legacy path.
|
|
3963
|
+
guard isNewVersion,
|
|
3964
|
+
peerWireCapsBinary,
|
|
3965
|
+
!wireHandshakeQueued,
|
|
3966
|
+
peerWireProtocolVersion < BleWireProtocol.protocolV2
|
|
3967
|
+
else { return }
|
|
3968
|
+
sendWireHandshake()
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
private func sendWireHandshake() {
|
|
3972
|
+
guard isNewVersion else { return }
|
|
3973
|
+
var flags = BleWireProtocol.flagHandshake
|
|
3974
|
+
flags |= BleWireProtocol.flagFirstFrag
|
|
3975
|
+
flags |= BleWireProtocol.flagLastFrag
|
|
3976
|
+
let payload = Data(BleWireProtocol.handshakePayloadV2.utf8)
|
|
3977
|
+
guard let packed = BleWireProtocol.packBinaryFragment(
|
|
3978
|
+
flags: flags,
|
|
3979
|
+
msgId: 0,
|
|
3980
|
+
fragIdx: 0,
|
|
3981
|
+
fragCount: 1,
|
|
3982
|
+
payload: payload
|
|
3983
|
+
) else {
|
|
3984
|
+
return
|
|
3985
|
+
}
|
|
3986
|
+
Bridge.log("LIVE: Sending BLE wire v2 handshake")
|
|
3987
|
+
wireHandshakeQueued = true
|
|
3988
|
+
queueSend(packed, id: "-1")
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
private func activateBinaryWireV2Session(logMessage: String) {
|
|
3992
|
+
peerWireProtocolVersion = BleWireProtocol.protocolV2
|
|
3993
|
+
useBinaryWireProtocol = true
|
|
3994
|
+
wireHandshakeQueued = false
|
|
3995
|
+
peerK900Le = true
|
|
3996
|
+
BleJsonCompact.markSessionConnected(epochMs: Int64(Date().timeIntervalSince1970 * 1000))
|
|
3997
|
+
Bridge.log(logMessage)
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
private func handlePeerWireHandshake() {
|
|
4001
|
+
activateBinaryWireV2Session(logMessage: "LIVE: Peer confirmed BLE wire protocol v2")
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
private func processBinaryWireFrame(_ data: Data) {
|
|
4005
|
+
guard let info = BleWireProtocol.extractBinaryFragmentInfo(data) else {
|
|
4006
|
+
Bridge.log("LIVE: Failed to parse binary wire frame")
|
|
4007
|
+
return
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
if BleWireProtocol.isHandshakeV2(info) {
|
|
4011
|
+
handlePeerWireHandshake()
|
|
4012
|
+
return
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
if !useBinaryWireProtocol, isNewVersion {
|
|
4016
|
+
activateBinaryWireV2Session(logMessage: "LIVE: Auto-enabled BLE wire v2 from incoming binary frame")
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
guard let reassembled = incomingChunkReassembler.addBinaryFragment(
|
|
4020
|
+
msgId: info.msgId,
|
|
4021
|
+
fragIdx: Int(info.fragIdx),
|
|
4022
|
+
fragCount: Int(info.fragCount),
|
|
4023
|
+
data: info.payload
|
|
4024
|
+
) else {
|
|
4025
|
+
return
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
guard let jsonString = String(data: reassembled, encoding: .utf8) else {
|
|
4029
|
+
Bridge.log("LIVE: Failed to decode reassembled binary wire payload")
|
|
4030
|
+
return
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
logWireMetrics(
|
|
4034
|
+
payloadBytes: reassembled.count,
|
|
4035
|
+
wireBytes: data.count,
|
|
4036
|
+
packetCount: Int(info.fragCount),
|
|
4037
|
+
protocolVersion: BleWireProtocol.protocolV2,
|
|
4038
|
+
direction: "glasses_to_phone"
|
|
4039
|
+
)
|
|
4040
|
+
processJsonMessage(jsonString)
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
private func compactWireJson(_ json: [String: Any]) -> [String: Any] {
|
|
4044
|
+
guard useBinaryWireProtocol, isNewVersion else { return json }
|
|
4045
|
+
return BleJsonCompact.encode(json)
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
private func expandCompactWireJson(_ json: [String: Any]) -> [String: Any]? {
|
|
4049
|
+
guard useBinaryWireProtocol, isNewVersion else { return json }
|
|
4050
|
+
return BleJsonCompact.decodeIfSupported(json)
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
private func logWireMetrics(
|
|
4054
|
+
payloadBytes: Int,
|
|
4055
|
+
wireBytes: Int,
|
|
4056
|
+
packetCount: Int,
|
|
4057
|
+
protocolVersion: Int,
|
|
4058
|
+
direction: String
|
|
4059
|
+
) {
|
|
4060
|
+
Bridge.log(
|
|
4061
|
+
"BLE_TRACE direction=\(direction) proto=v\(protocolVersion) payload=\(payloadBytes) wire=\(wireBytes) packets=\(packetCount)"
|
|
4062
|
+
)
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
private func sendJsonBinary(
|
|
4066
|
+
jsonString: String,
|
|
4067
|
+
messageId: Int64,
|
|
4068
|
+
trackingId: String,
|
|
4069
|
+
wakeUp: Bool,
|
|
4070
|
+
requireAck: Bool
|
|
4071
|
+
) {
|
|
4072
|
+
let payload = Data(jsonString.utf8)
|
|
4073
|
+
let msgId = UInt16(truncatingIfNeeded: messageId)
|
|
4074
|
+
let fragments = MessageChunker.createBinaryFragments(
|
|
4075
|
+
payload: payload,
|
|
4076
|
+
msgId: msgId,
|
|
4077
|
+
wakeUp: wakeUp,
|
|
4078
|
+
ackRequested: requireAck && messageId >= 0
|
|
4079
|
+
)
|
|
4080
|
+
guard !fragments.isEmpty else {
|
|
4081
|
+
Bridge.log("LIVE: Failed to create binary wire fragments")
|
|
4082
|
+
return
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
var totalWireBytes = 0
|
|
4086
|
+
for (index, fragment) in fragments.enumerated() {
|
|
4087
|
+
guard let packed = BleWireProtocol.packBinaryFragment(
|
|
4088
|
+
flags: fragment.flags,
|
|
4089
|
+
msgId: fragment.msgId,
|
|
4090
|
+
fragIdx: fragment.fragIdx,
|
|
4091
|
+
fragCount: fragment.fragCount,
|
|
4092
|
+
payload: fragment.payload
|
|
4093
|
+
) else {
|
|
4094
|
+
continue
|
|
4095
|
+
}
|
|
4096
|
+
totalWireBytes += packed.count
|
|
4097
|
+
let isFinalChunk = index == fragments.count - 1
|
|
4098
|
+
let chunkTrackingId = (requireAck && isFinalChunk) ? trackingId : "-1"
|
|
4099
|
+
queueSend(packed, id: chunkTrackingId)
|
|
4100
|
+
|
|
4101
|
+
if index < fragments.count - 1 {
|
|
4102
|
+
Thread.sleep(forTimeInterval: 0.05)
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
logWireMetrics(
|
|
4107
|
+
payloadBytes: payload.count,
|
|
4108
|
+
wireBytes: totalWireBytes,
|
|
4109
|
+
packetCount: fragments.count,
|
|
4110
|
+
protocolVersion: BleWireProtocol.protocolV2,
|
|
4111
|
+
direction: "phone_to_glasses"
|
|
4112
|
+
)
|
|
4113
|
+
Bridge.log("LIVE: Binary v2 queued \(fragments.count) fragments, wireBytes=\(totalWireBytes)")
|
|
4114
|
+
}
|
|
4115
|
+
|
|
3659
4116
|
func sendJson(_ jsonOriginal: [String: Any], wakeUp: Bool = false, requireAck: Bool = true) {
|
|
3660
4117
|
do {
|
|
3661
4118
|
var json = jsonOriginal
|
|
@@ -3669,8 +4126,27 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3669
4126
|
globalMessageId += 1
|
|
3670
4127
|
}
|
|
3671
4128
|
|
|
3672
|
-
let jsonData = try JSONSerialization.data(withJSONObject: json)
|
|
4129
|
+
let jsonData = try JSONSerialization.data(withJSONObject: compactWireJson(json))
|
|
3673
4130
|
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
4131
|
+
let commandInfo = outgoingBleCommandTraceInfo(json)
|
|
4132
|
+
BleTraceLogger.logJson(
|
|
4133
|
+
direction: "phone_to_glasses",
|
|
4134
|
+
layer: "sdk_ble_command",
|
|
4135
|
+
payload: json,
|
|
4136
|
+
bytes: jsonData.count
|
|
4137
|
+
)
|
|
4138
|
+
|
|
4139
|
+
if useBinaryWireProtocol, isNewVersion {
|
|
4140
|
+
sendJsonBinary(
|
|
4141
|
+
jsonString: jsonString,
|
|
4142
|
+
messageId: messageId,
|
|
4143
|
+
trackingId: trackingId,
|
|
4144
|
+
wakeUp: wakeUp,
|
|
4145
|
+
requireAck: requireAck
|
|
4146
|
+
)
|
|
4147
|
+
return
|
|
4148
|
+
}
|
|
4149
|
+
|
|
3674
4150
|
// First check if the message needs chunking
|
|
3675
4151
|
// Create a test C-wrapped version to check size
|
|
3676
4152
|
var testWrapper: [String: Any] = [K900ProtocolUtils.FIELD_C: jsonString]
|
|
@@ -3707,7 +4183,21 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3707
4183
|
// All other chunks get "-1" (no ACK tracking)
|
|
3708
4184
|
let isFinalChunk = (index == chunks.count - 1)
|
|
3709
4185
|
let chunkTrackingId = (requireAck && isFinalChunk) ? trackingId : "-1"
|
|
3710
|
-
|
|
4186
|
+
let trace = createBleWriteTrace(
|
|
4187
|
+
commandInfo: commandInfo,
|
|
4188
|
+
chunkId: chunk["id"] as? String,
|
|
4189
|
+
chunkIndex: index,
|
|
4190
|
+
totalChunks: chunks.count,
|
|
4191
|
+
payloadBytes: chunkStr.data(using: .utf8)?.count,
|
|
4192
|
+
packedBytes: packedData.count,
|
|
4193
|
+
wakeup: wakeUp && index == 0,
|
|
4194
|
+
chunked: true
|
|
4195
|
+
)
|
|
4196
|
+
logBleChunkTrace("created", trace, extra: [
|
|
4197
|
+
"chunkJsonBytes": chunkStr.data(using: .utf8)?.count,
|
|
4198
|
+
"messageBytes": jsonData.count,
|
|
4199
|
+
])
|
|
4200
|
+
queueSend(packedData, id: chunkTrackingId, trace: trace)
|
|
3711
4201
|
|
|
3712
4202
|
// Add small delay between chunks to avoid overwhelming the connection
|
|
3713
4203
|
if index < chunks.count - 1 {
|
|
@@ -3724,7 +4214,20 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3724
4214
|
}
|
|
3725
4215
|
Bridge.log("LIVE: Sending data to glasses: \(jsonString)")
|
|
3726
4216
|
let packedData = packJson(jsonString, wakeUp: wakeUp) ?? Data()
|
|
3727
|
-
|
|
4217
|
+
let trace = createBleWriteTrace(
|
|
4218
|
+
commandInfo: commandInfo,
|
|
4219
|
+
chunkId: nil,
|
|
4220
|
+
chunkIndex: nil,
|
|
4221
|
+
totalChunks: nil,
|
|
4222
|
+
payloadBytes: jsonData.count,
|
|
4223
|
+
packedBytes: packedData.count,
|
|
4224
|
+
wakeup: wakeUp,
|
|
4225
|
+
chunked: false
|
|
4226
|
+
)
|
|
4227
|
+
logBleChunkTrace("created", trace, extra: [
|
|
4228
|
+
"messageBytes": jsonData.count,
|
|
4229
|
+
])
|
|
4230
|
+
queueSend(packedData, id: trackingId, trace: trace)
|
|
3728
4231
|
}
|
|
3729
4232
|
}
|
|
3730
4233
|
} catch {
|
|
@@ -4397,6 +4900,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4397
4900
|
// Stop all timers
|
|
4398
4901
|
stopAllTimers()
|
|
4399
4902
|
incomingChunkReassembler.clear()
|
|
4903
|
+
peerWireProtocolVersion = 0
|
|
4904
|
+
useBinaryWireProtocol = false
|
|
4905
|
+
wireHandshakeQueued = false
|
|
4906
|
+
peerK900Le = false
|
|
4907
|
+
peerWireCapsBinary = false
|
|
4908
|
+
BleJsonCompact.resetSession()
|
|
4400
4909
|
|
|
4401
4910
|
// Disconnect BLE
|
|
4402
4911
|
if let peripheral = connectedPeripheral {
|
|
@@ -4455,12 +4964,58 @@ extension MentraLive {
|
|
|
4455
4964
|
return result
|
|
4456
4965
|
}
|
|
4457
4966
|
|
|
4967
|
+
/**
|
|
4968
|
+
* Heuristically detect the K900 STRING length field's endianness for a received frame. Mirrors
|
|
4969
|
+
* the firmware and Android codec: a length is plausible when it is non-zero, within the 512-byte
|
|
4970
|
+
* cap, and the full framed command fits inside the buffer; when both fit, prefer the one landing
|
|
4971
|
+
* on the trailing `$$`, then the smaller. Returns (length, isLittleEndian) or nil.
|
|
4972
|
+
*/
|
|
4973
|
+
private func k900DetectStringLength(_ bytes: [UInt8]) -> (length: Int, isLe: Bool)? {
|
|
4974
|
+
guard bytes.count >= 7 else { return nil }
|
|
4975
|
+
let leLen = Int(bytes[3]) | (Int(bytes[4]) << 8)
|
|
4976
|
+
let beLen = (Int(bytes[3]) << 8) | Int(bytes[4])
|
|
4977
|
+
|
|
4978
|
+
let leOk = leLen > 0 && leLen <= 512 && leLen + 7 <= bytes.count
|
|
4979
|
+
let beOk = beLen > 0 && beLen <= 512 && beLen + 7 <= bytes.count
|
|
4980
|
+
|
|
4981
|
+
func endsWithEndCode(_ end: Int) -> Bool {
|
|
4982
|
+
end + 1 < bytes.count && bytes[end] == 0x24 && bytes[end + 1] == 0x24
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
if leOk, beOk {
|
|
4986
|
+
if endsWithEndCode(5 + leLen) { return (leLen, true) }
|
|
4987
|
+
if endsWithEndCode(5 + beLen) { return (beLen, false) }
|
|
4988
|
+
return leLen <= beLen ? (leLen, true) : (beLen, false)
|
|
4989
|
+
}
|
|
4990
|
+
if leOk { return (leLen, true) }
|
|
4991
|
+
if beOk { return (beLen, false) }
|
|
4992
|
+
return nil
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4995
|
+
/**
|
|
4996
|
+
* Parse a `wire_caps` object from a version_info/glasses_ready message and update the negotiated
|
|
4997
|
+
* per-link endianness and binary support. Missing wire_caps leaves the legacy defaults (BE, no
|
|
4998
|
+
* binary) untouched so older glasses keep working.
|
|
4999
|
+
*/
|
|
5000
|
+
private func parsePeerWireCaps(_ json: [String: Any]) {
|
|
5001
|
+
guard let caps = json["wire_caps"] as? [String: Any] else { return }
|
|
5002
|
+
if (caps["k900_le"] as? Bool) == true {
|
|
5003
|
+
if !peerK900Le {
|
|
5004
|
+
peerK900Le = true
|
|
5005
|
+
Bridge.log("LIVE: wire_caps negotiated k900 endian=LE")
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
if caps.keys.contains("binary") {
|
|
5009
|
+
peerWireCapsBinary = (caps["binary"] as? Bool) == true
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
|
|
4458
5013
|
/**
|
|
4459
5014
|
* Pack raw byte data with K900 BES2700 protocol format for phone-to-device communication
|
|
4460
5015
|
* Format: ## + command_type + length(2bytes) + data + $$
|
|
4461
5016
|
* Uses little-endian byte order for length field
|
|
4462
5017
|
*/
|
|
4463
|
-
private func packDataToK900(_ data: Data?, cmdType: UInt8) -> Data? {
|
|
5018
|
+
private func packDataToK900(_ data: Data?, cmdType: UInt8, littleEndian: Bool = false) -> Data? {
|
|
4464
5019
|
guard let data else { return nil }
|
|
4465
5020
|
|
|
4466
5021
|
let dataLength = data.count
|
|
@@ -4474,9 +5029,14 @@ extension MentraLive {
|
|
|
4474
5029
|
// Command type
|
|
4475
5030
|
result.append(cmdType)
|
|
4476
5031
|
|
|
4477
|
-
// Length (2 bytes,
|
|
4478
|
-
|
|
4479
|
-
|
|
5032
|
+
// Length (2 bytes, negotiated endianness)
|
|
5033
|
+
if littleEndian {
|
|
5034
|
+
result.append(UInt8(dataLength & 0xFF)) // LSB first
|
|
5035
|
+
result.append(UInt8((dataLength >> 8) & 0xFF)) // MSB second
|
|
5036
|
+
} else {
|
|
5037
|
+
result.append(UInt8((dataLength >> 8) & 0xFF)) // MSB first
|
|
5038
|
+
result.append(UInt8(dataLength & 0xFF)) // LSB second
|
|
5039
|
+
}
|
|
4480
5040
|
|
|
4481
5041
|
// Copy the data
|
|
4482
5042
|
result.append(data)
|
|
@@ -4506,9 +5066,13 @@ extension MentraLive {
|
|
|
4506
5066
|
let jsonData = try JSONSerialization.data(withJSONObject: wrapper)
|
|
4507
5067
|
guard let wrappedJson = String(data: jsonData, encoding: .utf8) else { return nil }
|
|
4508
5068
|
|
|
4509
|
-
// Then pack with BES2700 protocol format using
|
|
5069
|
+
// Then pack with BES2700 protocol format using the negotiated endianness
|
|
4510
5070
|
let jsonBytes = wrappedJson.data(using: .utf8)!
|
|
4511
|
-
return packDataToK900(
|
|
5071
|
+
return packDataToK900(
|
|
5072
|
+
jsonBytes,
|
|
5073
|
+
cmdType: K900ProtocolUtils.CMD_TYPE_STRING,
|
|
5074
|
+
littleEndian: peerK900Le
|
|
5075
|
+
)
|
|
4512
5076
|
|
|
4513
5077
|
} catch {
|
|
4514
5078
|
Bridge.log("Error creating JSON wrapper for K900: \(error)")
|
|
@@ -4801,7 +5365,11 @@ extension MentraLive {
|
|
|
4801
5365
|
}
|
|
4802
5366
|
let commandData = try JSONSerialization.data(withJSONObject: payload)
|
|
4803
5367
|
guard
|
|
4804
|
-
let packet = packDataToK900(
|
|
5368
|
+
let packet = packDataToK900(
|
|
5369
|
+
commandData,
|
|
5370
|
+
cmdType: K900ProtocolUtils.CMD_TYPE_STRING,
|
|
5371
|
+
littleEndian: peerK900Le
|
|
5372
|
+
)
|
|
4805
5373
|
else {
|
|
4806
5374
|
Bridge.log("LIVE: Failed to pack raw K900 command")
|
|
4807
5375
|
return false
|