@mentra/bluetooth-sdk 0.1.18 → 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/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/Source/BluetoothSdkDefaults.swift +1 -1
- 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/sgcs/G2.swift +518 -11
- package/ios/Source/sgcs/MentraLive.swift +308 -30
- 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
|
|
@@ -1218,6 +1222,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1218
1222
|
// or stale lastBesOtaProgress on the next OTA).
|
|
1219
1223
|
if state == ConnTypes.DISCONNECTED {
|
|
1220
1224
|
incomingChunkReassembler.clear()
|
|
1225
|
+
peerWireProtocolVersion = 0
|
|
1226
|
+
useBinaryWireProtocol = false
|
|
1227
|
+
wireHandshakeQueued = false
|
|
1228
|
+
peerK900Le = false
|
|
1229
|
+
peerWireCapsBinary = false
|
|
1230
|
+
BleJsonCompact.resetSession()
|
|
1221
1231
|
stopSignalStrengthPolling()
|
|
1222
1232
|
DeviceStore.shared.apply("glasses", "signalStrength", -1)
|
|
1223
1233
|
DeviceStore.shared.apply("glasses", "signalStrengthUpdatedAt", 0)
|
|
@@ -1380,7 +1390,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1380
1390
|
private var connectedPeripheral: CBPeripheral?
|
|
1381
1391
|
private var txCharacteristic: CBCharacteristic?
|
|
1382
1392
|
private var rxCharacteristic: CBCharacteristic?
|
|
1383
|
-
private let bes2700MtuLimit =
|
|
1393
|
+
private let bes2700MtuLimit = 509
|
|
1384
1394
|
private var currentMtu: Int = 23 // Default BLE MTU
|
|
1385
1395
|
|
|
1386
1396
|
// State Tracking
|
|
@@ -1389,6 +1399,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1389
1399
|
private var isKilled = false
|
|
1390
1400
|
private var reconnectAttempts = 0
|
|
1391
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
|
|
1392
1410
|
private var globalMessageId = 0
|
|
1393
1411
|
private var lastReceivedMessageId = 0
|
|
1394
1412
|
private var bleWriteTraceSequence = 0
|
|
@@ -1513,7 +1531,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1513
1531
|
Bridge.log("Found already-connected peripheral: \(peripheral.name ?? "Unknown")")
|
|
1514
1532
|
if let name = peripheral.name,
|
|
1515
1533
|
name == "Xy_A" || name.hasPrefix("XyBLE_") || name.hasPrefix("MENTRA_LIVE_BLE")
|
|
1516
|
-
|| name.hasPrefix("MENTRA_LIVE_BT")
|
|
1534
|
+
|| name.hasPrefix("MENTRA_LIVE_BT") || name.lowercased().hasPrefix("mentra_live")
|
|
1517
1535
|
{
|
|
1518
1536
|
Bridge.log("Found already-connected peripheral: \(name)")
|
|
1519
1537
|
discoveredPeripherals[name] = peripheral
|
|
@@ -2106,17 +2124,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2106
2124
|
return // Exit after processing file packet
|
|
2107
2125
|
}
|
|
2108
2126
|
|
|
2109
|
-
|
|
2127
|
+
if commandType == K900ProtocolUtils.CMD_TYPE_BINARY_MSG {
|
|
2128
|
+
processBinaryWireFrame(data)
|
|
2129
|
+
return
|
|
2130
|
+
}
|
|
2110
2131
|
|
|
2111
|
-
//
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
} else {
|
|
2118
|
-
// Standard device - little-endian
|
|
2119
|
-
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
|
|
2120
2138
|
}
|
|
2121
2139
|
|
|
2122
2140
|
// Bridge.log(
|
|
@@ -2150,7 +2168,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2150
2168
|
}
|
|
2151
2169
|
}
|
|
2152
2170
|
|
|
2153
|
-
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
|
+
}
|
|
2154
2176
|
// Log ALL incoming JSON objects for debugging
|
|
2155
2177
|
// Bridge.log("LIVE: DEBUG: processJsonObject: \(json)")
|
|
2156
2178
|
BleTraceLogger.logJson(direction: "glasses_to_phone", layer: "sdk_ble_event", payload: json)
|
|
@@ -2199,12 +2221,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2199
2221
|
|
|
2200
2222
|
switch type {
|
|
2201
2223
|
case "glasses_ready":
|
|
2224
|
+
parsePeerWireCaps(json)
|
|
2202
2225
|
handleGlassesReady()
|
|
2203
2226
|
|
|
2204
2227
|
case "battery_status":
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
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)
|
|
2208
2234
|
|
|
2209
2235
|
case "voice_activity_detection_status":
|
|
2210
2236
|
let enabled = json["voiceActivityDetectionEnabled"] as? Bool
|
|
@@ -2219,6 +2245,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2219
2245
|
let connected = json["connected"] as? Bool ?? false
|
|
2220
2246
|
let ssid = json["ssid"] as? String ?? ""
|
|
2221
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
|
+
}
|
|
2222
2259
|
updateWifiStatus(connected: connected, ssid: ssid, ip: ip)
|
|
2223
2260
|
|
|
2224
2261
|
case "hotspot_status_update":
|
|
@@ -2463,6 +2500,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2463
2500
|
if let buildNumber = fields["build_number"] as? String {
|
|
2464
2501
|
isNewVersion = (Int(buildNumber) ?? 0) >= 5
|
|
2465
2502
|
DeviceStore.shared.apply("glasses", "buildNumber", buildNumber)
|
|
2503
|
+
parsePeerWireCaps(json)
|
|
2504
|
+
maybeSendWireHandshake()
|
|
2466
2505
|
}
|
|
2467
2506
|
if let deviceModel = fields["device_model"] as? String {
|
|
2468
2507
|
DeviceStore.shared.apply("glasses", "deviceModel", deviceModel)
|
|
@@ -2595,10 +2634,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2595
2634
|
if let bodyObj = json["B"] as? [String: Any] {
|
|
2596
2635
|
let readyResponse = bodyObj["ready"] as? Int ?? 0
|
|
2597
2636
|
|
|
2598
|
-
// 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.
|
|
2599
2640
|
let percentage = bodyObj["pt"] as? Int ?? 0
|
|
2600
2641
|
let voltage = bodyObj["vt"] as? Int ?? 0
|
|
2601
|
-
let
|
|
2642
|
+
let chargBit = bodyObj["charg"] as? Int
|
|
2643
|
+
let charging = chargBit != nil ? (chargBit == 1) : self.charging
|
|
2602
2644
|
|
|
2603
2645
|
// SOC is still booting
|
|
2604
2646
|
if readyResponse == 0 {
|
|
@@ -2646,12 +2688,15 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2646
2688
|
let percentage = body["pt"] as? Int
|
|
2647
2689
|
{
|
|
2648
2690
|
let voltageVolts = Double(voltage) / 1000.0
|
|
2649
|
-
let isCharging = voltage > 4000
|
|
2650
2691
|
|
|
2651
2692
|
Bridge.log(
|
|
2652
2693
|
"🔋 K900 Battery Status - Voltage: \(voltageVolts)V, Level: \(percentage)%"
|
|
2653
2694
|
)
|
|
2654
|
-
|
|
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)
|
|
2655
2700
|
}
|
|
2656
2701
|
|
|
2657
2702
|
case "sr_getvol":
|
|
@@ -3057,6 +3102,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3057
3102
|
DeviceStore.shared.apply("glasses", "firmwareVersion", firmwareVersion)
|
|
3058
3103
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
3059
3104
|
isNewVersion = (Int(buildNumber) ?? 0) >= 5
|
|
3105
|
+
maybeSendWireHandshake()
|
|
3060
3106
|
DeviceStore.shared.apply("glasses", "deviceModel", deviceModel)
|
|
3061
3107
|
DeviceStore.shared.apply("glasses", "androidVersion", androidVersion)
|
|
3062
3108
|
|
|
@@ -3566,7 +3612,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3566
3612
|
let basePath = components.path.hasSuffix("/")
|
|
3567
3613
|
? String(components.path.dropLast())
|
|
3568
3614
|
: components.path
|
|
3569
|
-
components.path = basePath + "/api/
|
|
3615
|
+
components.path = basePath + "/api/client/reports/\(relay.incidentId)/artifacts"
|
|
3570
3616
|
guard let url = components.url else {
|
|
3571
3617
|
sendTransferCompleteConfirmation(fileName: fileName, success: false)
|
|
3572
3618
|
if let existing = bleIncidentLogRelays[relay.fileBaseKey] {
|
|
@@ -3911,6 +3957,162 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3911
3957
|
}
|
|
3912
3958
|
}
|
|
3913
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
|
+
|
|
3914
4116
|
func sendJson(_ jsonOriginal: [String: Any], wakeUp: Bool = false, requireAck: Bool = true) {
|
|
3915
4117
|
do {
|
|
3916
4118
|
var json = jsonOriginal
|
|
@@ -3924,7 +4126,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3924
4126
|
globalMessageId += 1
|
|
3925
4127
|
}
|
|
3926
4128
|
|
|
3927
|
-
let jsonData = try JSONSerialization.data(withJSONObject: json)
|
|
4129
|
+
let jsonData = try JSONSerialization.data(withJSONObject: compactWireJson(json))
|
|
3928
4130
|
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
3929
4131
|
let commandInfo = outgoingBleCommandTraceInfo(json)
|
|
3930
4132
|
BleTraceLogger.logJson(
|
|
@@ -3934,6 +4136,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3934
4136
|
bytes: jsonData.count
|
|
3935
4137
|
)
|
|
3936
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
|
+
|
|
3937
4150
|
// First check if the message needs chunking
|
|
3938
4151
|
// Create a test C-wrapped version to check size
|
|
3939
4152
|
var testWrapper: [String: Any] = [K900ProtocolUtils.FIELD_C: jsonString]
|
|
@@ -4687,6 +4900,12 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4687
4900
|
// Stop all timers
|
|
4688
4901
|
stopAllTimers()
|
|
4689
4902
|
incomingChunkReassembler.clear()
|
|
4903
|
+
peerWireProtocolVersion = 0
|
|
4904
|
+
useBinaryWireProtocol = false
|
|
4905
|
+
wireHandshakeQueued = false
|
|
4906
|
+
peerK900Le = false
|
|
4907
|
+
peerWireCapsBinary = false
|
|
4908
|
+
BleJsonCompact.resetSession()
|
|
4690
4909
|
|
|
4691
4910
|
// Disconnect BLE
|
|
4692
4911
|
if let peripheral = connectedPeripheral {
|
|
@@ -4745,12 +4964,58 @@ extension MentraLive {
|
|
|
4745
4964
|
return result
|
|
4746
4965
|
}
|
|
4747
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
|
+
|
|
4748
5013
|
/**
|
|
4749
5014
|
* Pack raw byte data with K900 BES2700 protocol format for phone-to-device communication
|
|
4750
5015
|
* Format: ## + command_type + length(2bytes) + data + $$
|
|
4751
5016
|
* Uses little-endian byte order for length field
|
|
4752
5017
|
*/
|
|
4753
|
-
private func packDataToK900(_ data: Data?, cmdType: UInt8) -> Data? {
|
|
5018
|
+
private func packDataToK900(_ data: Data?, cmdType: UInt8, littleEndian: Bool = false) -> Data? {
|
|
4754
5019
|
guard let data else { return nil }
|
|
4755
5020
|
|
|
4756
5021
|
let dataLength = data.count
|
|
@@ -4764,9 +5029,14 @@ extension MentraLive {
|
|
|
4764
5029
|
// Command type
|
|
4765
5030
|
result.append(cmdType)
|
|
4766
5031
|
|
|
4767
|
-
// Length (2 bytes,
|
|
4768
|
-
|
|
4769
|
-
|
|
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
|
+
}
|
|
4770
5040
|
|
|
4771
5041
|
// Copy the data
|
|
4772
5042
|
result.append(data)
|
|
@@ -4796,9 +5066,13 @@ extension MentraLive {
|
|
|
4796
5066
|
let jsonData = try JSONSerialization.data(withJSONObject: wrapper)
|
|
4797
5067
|
guard let wrappedJson = String(data: jsonData, encoding: .utf8) else { return nil }
|
|
4798
5068
|
|
|
4799
|
-
// Then pack with BES2700 protocol format using
|
|
5069
|
+
// Then pack with BES2700 protocol format using the negotiated endianness
|
|
4800
5070
|
let jsonBytes = wrappedJson.data(using: .utf8)!
|
|
4801
|
-
return packDataToK900(
|
|
5071
|
+
return packDataToK900(
|
|
5072
|
+
jsonBytes,
|
|
5073
|
+
cmdType: K900ProtocolUtils.CMD_TYPE_STRING,
|
|
5074
|
+
littleEndian: peerK900Le
|
|
5075
|
+
)
|
|
4802
5076
|
|
|
4803
5077
|
} catch {
|
|
4804
5078
|
Bridge.log("Error creating JSON wrapper for K900: \(error)")
|
|
@@ -5091,7 +5365,11 @@ extension MentraLive {
|
|
|
5091
5365
|
}
|
|
5092
5366
|
let commandData = try JSONSerialization.data(withJSONObject: payload)
|
|
5093
5367
|
guard
|
|
5094
|
-
let packet = packDataToK900(
|
|
5368
|
+
let packet = packDataToK900(
|
|
5369
|
+
commandData,
|
|
5370
|
+
cmdType: K900ProtocolUtils.CMD_TYPE_STRING,
|
|
5371
|
+
littleEndian: peerK900Le
|
|
5372
|
+
)
|
|
5095
5373
|
else {
|
|
5096
5374
|
Bridge.log("LIVE: Failed to pack raw K900 command")
|
|
5097
5375
|
return false
|