@mentra/bluetooth-sdk 0.1.18 → 0.1.20
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/BluetoothSdkModule.kt +135 -51
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +120 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +551 -159
- 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/types/DeviceModels.kt +2 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -5
- 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 +109 -73
- 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/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -9
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- 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/BlePhotoUploadServiceTest.java +9 -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 +37 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +1 -0
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -0
- package/build/index.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 +136 -30
- package/ios/Source/DeviceStore.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +40 -24
- package/ios/Source/OtaManifest.swift +1 -1
- package/ios/Source/camera/CameraModels.swift +17 -3
- package/ios/Source/sgcs/G1.swift +1 -1
- package/ios/Source/sgcs/G2.swift +518 -11
- package/ios/Source/sgcs/MentraLive.swift +443 -42
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- 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/G1Text.swift +30 -1
- 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 +39 -1
- package/src/_private/BluetoothSdkModule.ts +4 -0
- package/src/_private/photoRequestPayload.ts +1 -0
- package/src/index.ts +6 -0
- package/src/types/index.ts +7 -0
|
@@ -104,6 +104,13 @@ class BlePhotoUploadService {
|
|
|
104
104
|
|
|
105
105
|
private static func convertToJpegPreservingExif(imageData: Data) throws -> Data {
|
|
106
106
|
logIncomingImageDiagnostics(imageData: imageData)
|
|
107
|
+
if isJpeg(imageData) {
|
|
108
|
+
Bridge.log(
|
|
109
|
+
"\(TAG): BLE relay pass-through: input already JPEG (\(imageData.count) bytes), skipping decode/re-encode"
|
|
110
|
+
)
|
|
111
|
+
return imageData
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
let imuJson = readImuJsonFromImageData(imageData)
|
|
108
115
|
|
|
109
116
|
guard let image = decodeImage(imageData: imageData) else {
|
|
@@ -118,6 +125,8 @@ class BlePhotoUploadService {
|
|
|
118
125
|
"\(TAG): Decoded image to bitmap: \(Int(image.size.width))x\(Int(image.size.height))"
|
|
119
126
|
)
|
|
120
127
|
|
|
128
|
+
// AVIF sources already went through a lossy pass on the glasses; re-encode
|
|
129
|
+
// at high-but-not-max quality. JPEG fast-path payloads upload as-is.
|
|
121
130
|
guard var jpegData = image.jpegData(compressionQuality: 0.9) else {
|
|
122
131
|
throw NSError(
|
|
123
132
|
domain: "BlePhotoUpload",
|
|
@@ -145,6 +154,11 @@ class BlePhotoUploadService {
|
|
|
145
154
|
)
|
|
146
155
|
}
|
|
147
156
|
|
|
157
|
+
private static func isJpeg(_ data: Data) -> Bool {
|
|
158
|
+
let bytes = [UInt8](data.prefix(2))
|
|
159
|
+
return bytes.count >= 2 && bytes[0] == 0xFF && bytes[1] == 0xD8
|
|
160
|
+
}
|
|
161
|
+
|
|
148
162
|
private static func describeContainer(_ data: Data) -> String {
|
|
149
163
|
let bytes = [UInt8](data.prefix(12))
|
|
150
164
|
if bytes.count >= 2, bytes[0] == 0xFF, bytes[1] == 0xD8 { return "jpeg" }
|
|
@@ -492,11 +506,12 @@ extension Data {
|
|
|
492
506
|
}
|
|
493
507
|
}
|
|
494
508
|
|
|
495
|
-
|
|
509
|
+
enum K900ProtocolUtils {
|
|
496
510
|
// Protocol constants
|
|
497
511
|
static let CMD_START_CODE: [UInt8] = [0x23, 0x23] // ##
|
|
498
512
|
static let CMD_END_CODE: [UInt8] = [0x24, 0x24] // $$
|
|
499
513
|
static let CMD_TYPE_STRING: UInt8 = 0x30 // String/JSON type
|
|
514
|
+
static let CMD_TYPE_BINARY_MSG: UInt8 = 0x40
|
|
500
515
|
|
|
501
516
|
// JSON Field constants
|
|
502
517
|
static let FIELD_C = "C" // Command/Content field
|
|
@@ -511,7 +526,9 @@ private enum K900ProtocolUtils {
|
|
|
511
526
|
static let CMD_TYPE_DATA: UInt8 = 0x35
|
|
512
527
|
|
|
513
528
|
// File transfer constants
|
|
514
|
-
|
|
529
|
+
// Negotiated file protocol ceiling. Legacy/GATT transfers remain smaller; 800-byte frames are
|
|
530
|
+
// accepted only after file_payload_v2 negotiation and an open CoC channel.
|
|
531
|
+
static let FILE_PACK_SIZE = 800
|
|
515
532
|
static let LENGTH_FILE_START = 2
|
|
516
533
|
static let LENGTH_FILE_TYPE = 1
|
|
517
534
|
static let LENGTH_FILE_PACKSIZE = 2
|
|
@@ -614,7 +631,7 @@ private enum K900ProtocolUtils {
|
|
|
614
631
|
print(
|
|
615
632
|
"K900ProtocolUtils: File packet checksum failed. Expected: \(String(format: "%02X", info.verifyCode)), Calculated: \(String(format: "%02X", calculatedVerify))"
|
|
616
633
|
)
|
|
617
|
-
} else {
|
|
634
|
+
} else if shouldLogFilePacket(info) {
|
|
618
635
|
print(
|
|
619
636
|
"K900ProtocolUtils: File packet extracted successfully: index=\(info.packIndex), size=\(info.packSize), fileName=\(info.fileName)"
|
|
620
637
|
)
|
|
@@ -622,6 +639,14 @@ private enum K900ProtocolUtils {
|
|
|
622
639
|
|
|
623
640
|
return info
|
|
624
641
|
}
|
|
642
|
+
|
|
643
|
+
static func shouldLogFilePacket(_ info: FilePacketInfo) -> Bool {
|
|
644
|
+
let totalPackets =
|
|
645
|
+
(Int(info.fileSize) + FILE_PACK_SIZE - 1) / FILE_PACK_SIZE
|
|
646
|
+
return info.packIndex == 0
|
|
647
|
+
|| info.packIndex % 32 == 0
|
|
648
|
+
|| Int(info.packIndex) >= max(totalPackets - 1, 0)
|
|
649
|
+
}
|
|
625
650
|
}
|
|
626
651
|
|
|
627
652
|
private struct FileTransferSession {
|
|
@@ -917,6 +942,7 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
917
942
|
self.rgbLedAuthorityClaimed = false
|
|
918
943
|
|
|
919
944
|
self.stopAllTimers()
|
|
945
|
+
self.closeL2capFileChannel()
|
|
920
946
|
|
|
921
947
|
// Clean up characteristics
|
|
922
948
|
self.txCharacteristic = nil
|
|
@@ -937,6 +963,7 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
937
963
|
|
|
938
964
|
self.stopConnectionTimeout()
|
|
939
965
|
self.isConnecting = false
|
|
966
|
+
self.closeL2capFileChannel()
|
|
940
967
|
self.connectedPeripheral = nil
|
|
941
968
|
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
942
969
|
|
|
@@ -1084,6 +1111,29 @@ extension MentraLive: CBPeripheralDelegate {
|
|
|
1084
1111
|
}
|
|
1085
1112
|
}
|
|
1086
1113
|
|
|
1114
|
+
nonisolated func peripheral(
|
|
1115
|
+
_ peripheral: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error?
|
|
1116
|
+
) {
|
|
1117
|
+
let errorDescription = error?.localizedDescription
|
|
1118
|
+
DispatchQueue.main.async { [weak self] in
|
|
1119
|
+
guard let self else { return }
|
|
1120
|
+
self.l2capFileChannelOpening = false
|
|
1121
|
+
|
|
1122
|
+
if let errorDescription {
|
|
1123
|
+
Bridge.log(
|
|
1124
|
+
"LIVE: L2CAP: unavailable, staying on GATT (\(errorDescription))"
|
|
1125
|
+
)
|
|
1126
|
+
return
|
|
1127
|
+
}
|
|
1128
|
+
guard peripheral === self.connectedPeripheral, let channel else {
|
|
1129
|
+
Bridge.log("LIVE: L2CAP: open completed for a stale connection")
|
|
1130
|
+
return
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
self.attachL2capFileChannel(channel)
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1087
1137
|
nonisolated func peripheral(
|
|
1088
1138
|
_: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?
|
|
1089
1139
|
) {
|
|
@@ -1218,6 +1268,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1218
1268
|
// or stale lastBesOtaProgress on the next OTA).
|
|
1219
1269
|
if state == ConnTypes.DISCONNECTED {
|
|
1220
1270
|
incomingChunkReassembler.clear()
|
|
1271
|
+
peerWireProtocolVersion = 0
|
|
1272
|
+
useBinaryWireProtocol = false
|
|
1273
|
+
wireHandshakeQueued = false
|
|
1274
|
+
peerK900Le = false
|
|
1275
|
+
peerWireCapsBinary = false
|
|
1276
|
+
peerFilePayloadV2 = false
|
|
1277
|
+
BleJsonCompact.resetSession()
|
|
1221
1278
|
stopSignalStrengthPolling()
|
|
1222
1279
|
DeviceStore.shared.apply("glasses", "signalStrength", -1)
|
|
1223
1280
|
DeviceStore.shared.apply("glasses", "signalStrengthUpdatedAt", 0)
|
|
@@ -1318,6 +1375,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1318
1375
|
private let TX_CHAR_UUID = CBUUID(string: "000071FF-0000-1000-8000-00805f9b34fb") // Central transmits on peripheral's RX
|
|
1319
1376
|
private let FILE_READ_UUID = CBUUID(string: "000072FF-0000-1000-8000-00805f9b34fb")
|
|
1320
1377
|
private let FILE_WRITE_UUID = CBUUID(string: "000073FF-0000-1000-8000-00805f9b34fb")
|
|
1378
|
+
private let L2CAP_FILE_PSM: CBL2CAPPSM = 0x00C9
|
|
1321
1379
|
|
|
1322
1380
|
// LC3 Audio UUIDs (for K901+ devices with microphone support)
|
|
1323
1381
|
private let LC3_READ_UUID = CBUUID(string: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
@@ -1331,6 +1389,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1331
1389
|
private var activeFileTransfers = [String: FileTransferSession]()
|
|
1332
1390
|
private var blePhotoTransfers = [String: BlePhotoTransfer]()
|
|
1333
1391
|
private var bleIncidentLogRelays = [String: BleIncidentLogRelayEntry]()
|
|
1392
|
+
private var l2capFileChannel: MentraLiveL2capChannel?
|
|
1393
|
+
private var l2capFileChannelId: UUID?
|
|
1394
|
+
private var l2capFileChannelOpening = false
|
|
1334
1395
|
private var rgbLedAuthorityClaimed = false
|
|
1335
1396
|
|
|
1336
1397
|
// LC3 Audio properties
|
|
@@ -1380,7 +1441,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1380
1441
|
private var connectedPeripheral: CBPeripheral?
|
|
1381
1442
|
private var txCharacteristic: CBCharacteristic?
|
|
1382
1443
|
private var rxCharacteristic: CBCharacteristic?
|
|
1383
|
-
private let bes2700MtuLimit =
|
|
1444
|
+
private let bes2700MtuLimit = 509
|
|
1384
1445
|
private var currentMtu: Int = 23 // Default BLE MTU
|
|
1385
1446
|
|
|
1386
1447
|
// State Tracking
|
|
@@ -1389,6 +1450,15 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1389
1450
|
private var isKilled = false
|
|
1390
1451
|
private var reconnectAttempts = 0
|
|
1391
1452
|
private var isNewVersion = false
|
|
1453
|
+
private var peerWireProtocolVersion = 0
|
|
1454
|
+
private var useBinaryWireProtocol = false
|
|
1455
|
+
private var wireHandshakeQueued = false
|
|
1456
|
+
// Negotiated K900 STRING length endianness for the phone<->glasses BLE link. Defaults to legacy
|
|
1457
|
+
// big-endian; upgraded to little-endian only when the glasses advertise wire_caps.k900_le (or a
|
|
1458
|
+
// v2 binary handshake succeeds, which implies wire-v2 LE).
|
|
1459
|
+
private var peerK900Le = false
|
|
1460
|
+
private var peerWireCapsBinary = false
|
|
1461
|
+
private var peerFilePayloadV2 = false
|
|
1392
1462
|
private var globalMessageId = 0
|
|
1393
1463
|
private var lastReceivedMessageId = 0
|
|
1394
1464
|
private var bleWriteTraceSequence = 0
|
|
@@ -1513,7 +1583,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1513
1583
|
Bridge.log("Found already-connected peripheral: \(peripheral.name ?? "Unknown")")
|
|
1514
1584
|
if let name = peripheral.name,
|
|
1515
1585
|
name == "Xy_A" || name.hasPrefix("XyBLE_") || name.hasPrefix("MENTRA_LIVE_BLE")
|
|
1516
|
-
|| name.hasPrefix("MENTRA_LIVE_BT")
|
|
1586
|
+
|| name.hasPrefix("MENTRA_LIVE_BT") || name.lowercased().hasPrefix("mentra_live")
|
|
1517
1587
|
{
|
|
1518
1588
|
Bridge.log("Found already-connected peripheral: \(name)")
|
|
1519
1589
|
discoveredPeripherals[name] = peripheral
|
|
@@ -1623,7 +1693,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1623
1693
|
|
|
1624
1694
|
func requestPhoto(_ request: PhotoRequest) {
|
|
1625
1695
|
Bridge.log(
|
|
1626
|
-
"LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(request.requestId) save=\(request.save) sound=\(request.sound) iso=\(request.iso.map { String($0) } ?? "auto") aeDivisor=\(request.aeExposureDivisor.map { String($0) } ?? "nil")"
|
|
1696
|
+
"LIVE: PHOTO PIPELINE [5/6] requestPhoto() entry requestId=\(request.requestId) mode=\(request.mode.rawValue) save=\(request.save) sound=\(request.sound) iso=\(request.iso.map { String($0) } ?? "auto") aeDivisor=\(request.aeExposureDivisor.map { String($0) } ?? "nil")"
|
|
1627
1697
|
)
|
|
1628
1698
|
|
|
1629
1699
|
var json: [String: Any] = [
|
|
@@ -1660,6 +1730,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1660
1730
|
let allowedSizes = ["low", "medium", "high", "max"]
|
|
1661
1731
|
let size = request.size.rawValue
|
|
1662
1732
|
json["size"] = allowedSizes.contains(size) ? size : "medium"
|
|
1733
|
+
json["mode"] = request.mode.rawValue
|
|
1663
1734
|
|
|
1664
1735
|
json["compress"] = request.compress?.rawValue ?? "none"
|
|
1665
1736
|
json["save"] = request.save
|
|
@@ -2049,6 +2120,56 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2049
2120
|
)
|
|
2050
2121
|
}
|
|
2051
2122
|
|
|
2123
|
+
// MARK: - L2CAP file channel
|
|
2124
|
+
|
|
2125
|
+
/// Open the BES file CoC. CoreBluetooth does not expose PHY or connection-priority
|
|
2126
|
+
/// controls, so BES owns those link updates when this channel is established.
|
|
2127
|
+
/// Any open failure leaves FILE_READ GATT notifications active as the fallback.
|
|
2128
|
+
private func openL2capFileChannel() {
|
|
2129
|
+
guard !l2capFileChannelOpening, l2capFileChannel == nil else { return }
|
|
2130
|
+
guard let peripheral = connectedPeripheral else { return }
|
|
2131
|
+
|
|
2132
|
+
l2capFileChannelOpening = true
|
|
2133
|
+
Bridge.log("LIVE: L2CAP: opening file channel (PSM 0xC9)")
|
|
2134
|
+
peripheral.openL2CAPChannel(L2CAP_FILE_PSM)
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
private func attachL2capFileChannel(_ channel: CBL2CAPChannel) {
|
|
2138
|
+
closeL2capFileChannel()
|
|
2139
|
+
|
|
2140
|
+
let channelId = UUID()
|
|
2141
|
+
l2capFileChannelId = channelId
|
|
2142
|
+
let reader = MentraLiveL2capChannel(
|
|
2143
|
+
channel: channel,
|
|
2144
|
+
onFileFrame: { [weak self] frame in
|
|
2145
|
+
// The reader thread keeps draining the stream (and returning CoC credits)
|
|
2146
|
+
// while the existing transfer state remains serialized on the main actor.
|
|
2147
|
+
DispatchQueue.main.async {
|
|
2148
|
+
self?.processReceivedData(frame)
|
|
2149
|
+
}
|
|
2150
|
+
},
|
|
2151
|
+
onClose: { [weak self] in
|
|
2152
|
+
DispatchQueue.main.async {
|
|
2153
|
+
guard let self, self.l2capFileChannelId == channelId else { return }
|
|
2154
|
+
self.l2capFileChannel = nil
|
|
2155
|
+
self.l2capFileChannelId = nil
|
|
2156
|
+
Bridge.log("LIVE: L2CAP: channel closed; using GATT fallback")
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
)
|
|
2160
|
+
l2capFileChannel = reader
|
|
2161
|
+
reader.start()
|
|
2162
|
+
Bridge.log("LIVE: L2CAP: channel open (PSM 0xC9)")
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
private func closeL2capFileChannel() {
|
|
2166
|
+
l2capFileChannelOpening = false
|
|
2167
|
+
l2capFileChannelId = nil
|
|
2168
|
+
let reader = l2capFileChannel
|
|
2169
|
+
l2capFileChannel = nil
|
|
2170
|
+
reader?.close()
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2052
2173
|
// MARK: - Data Processing
|
|
2053
2174
|
|
|
2054
2175
|
private func processReceivedData(_ data: Data) {
|
|
@@ -2087,10 +2208,6 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2087
2208
|
|| commandType == K900ProtocolUtils.CMD_TYPE_AUDIO
|
|
2088
2209
|
|| commandType == K900ProtocolUtils.CMD_TYPE_DATA
|
|
2089
2210
|
{
|
|
2090
|
-
Bridge.log(
|
|
2091
|
-
"📦 DETECTED FILE TRANSFER PACKET (type: 0x\(String(format: "%02X", commandType)))"
|
|
2092
|
-
)
|
|
2093
|
-
|
|
2094
2211
|
// Debug: Log the raw data
|
|
2095
2212
|
// let hexDump = data.prefix(64).map { String(format: "%02X ", $0) }.joined()
|
|
2096
2213
|
// Bridge.log("📦 Raw file packet data length=\(data.count), first 64 bytes: \(hexDump)")
|
|
@@ -2106,17 +2223,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2106
2223
|
return // Exit after processing file packet
|
|
2107
2224
|
}
|
|
2108
2225
|
|
|
2109
|
-
|
|
2226
|
+
if commandType == K900ProtocolUtils.CMD_TYPE_BINARY_MSG {
|
|
2227
|
+
processBinaryWireFrame(data)
|
|
2228
|
+
return
|
|
2229
|
+
}
|
|
2110
2230
|
|
|
2111
|
-
//
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
} else {
|
|
2118
|
-
// Standard device - little-endian
|
|
2119
|
-
payloadLength = (Int(bytes[4]) << 8) | Int(bytes[3])
|
|
2231
|
+
// Auto-detect the length endianness so we parse both legacy big-endian and wire-v2
|
|
2232
|
+
// little-endian frames, and learn the glasses' endianness for future outbound frames.
|
|
2233
|
+
let detected = k900DetectStringLength(bytes)
|
|
2234
|
+
let payloadLength = detected?.length ?? (Int(bytes[3]) | (Int(bytes[4]) << 8))
|
|
2235
|
+
if let detected {
|
|
2236
|
+
peerK900Le = detected.isLe
|
|
2120
2237
|
}
|
|
2121
2238
|
|
|
2122
2239
|
// Bridge.log(
|
|
@@ -2150,7 +2267,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2150
2267
|
}
|
|
2151
2268
|
}
|
|
2152
2269
|
|
|
2153
|
-
private func processJsonObject(_
|
|
2270
|
+
private func processJsonObject(_ incoming: [String: Any]) {
|
|
2271
|
+
guard let json = expandCompactWireJson(incoming) else {
|
|
2272
|
+
Bridge.log("LIVE: Rejected unsupported compact wire form")
|
|
2273
|
+
return
|
|
2274
|
+
}
|
|
2154
2275
|
// Log ALL incoming JSON objects for debugging
|
|
2155
2276
|
// Bridge.log("LIVE: DEBUG: processJsonObject: \(json)")
|
|
2156
2277
|
BleTraceLogger.logJson(direction: "glasses_to_phone", layer: "sdk_ble_event", payload: json)
|
|
@@ -2199,12 +2320,16 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2199
2320
|
|
|
2200
2321
|
switch type {
|
|
2201
2322
|
case "glasses_ready":
|
|
2323
|
+
parsePeerWireCaps(json)
|
|
2202
2324
|
handleGlassesReady()
|
|
2203
2325
|
|
|
2204
2326
|
case "battery_status":
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2327
|
+
// Percent only (the glasses send it as "percent"; the old "level" read never
|
|
2328
|
+
// matched). battery_status derives from hm_batv, which carries no charge bit —
|
|
2329
|
+
// older glasses fabricate `charging` from a voltage threshold. Charging state
|
|
2330
|
+
// comes exclusively from the PMU charg bit in the sr_hrt heartbeat.
|
|
2331
|
+
let level = json["percent"] as? Int ?? json["level"] as? Int ?? batteryLevel
|
|
2332
|
+
updateBatteryStatus(level: level, isCharging: charging)
|
|
2208
2333
|
|
|
2209
2334
|
case "voice_activity_detection_status":
|
|
2210
2335
|
let enabled = json["voiceActivityDetectionEnabled"] as? Bool
|
|
@@ -2219,6 +2344,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2219
2344
|
let connected = json["connected"] as? Bool ?? false
|
|
2220
2345
|
let ssid = json["ssid"] as? String ?? ""
|
|
2221
2346
|
let ip = json["local_ip"] as? String ?? ""
|
|
2347
|
+
// Provisioning failure reason (e.g. connect_timeout). Sticky: routine
|
|
2348
|
+
// error-less status updates (link-state debounce, request_wifi_status)
|
|
2349
|
+
// must not clear a failure nothing recovered from — only a newer error
|
|
2350
|
+
// or a successful connection overwrites it.
|
|
2351
|
+
let wifiError = json["error"] as? String ?? ""
|
|
2352
|
+
if !wifiError.isEmpty {
|
|
2353
|
+
Bridge.log("LIVE: 🌐 WiFi provisioning error from glasses: \(wifiError)")
|
|
2354
|
+
DeviceStore.shared.apply("glasses", "wifiError", wifiError)
|
|
2355
|
+
} else if connected {
|
|
2356
|
+
DeviceStore.shared.apply("glasses", "wifiError", "")
|
|
2357
|
+
}
|
|
2222
2358
|
updateWifiStatus(connected: connected, ssid: ssid, ip: ip)
|
|
2223
2359
|
|
|
2224
2360
|
case "hotspot_status_update":
|
|
@@ -2463,6 +2599,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2463
2599
|
if let buildNumber = fields["build_number"] as? String {
|
|
2464
2600
|
isNewVersion = (Int(buildNumber) ?? 0) >= 5
|
|
2465
2601
|
DeviceStore.shared.apply("glasses", "buildNumber", buildNumber)
|
|
2602
|
+
parsePeerWireCaps(json)
|
|
2603
|
+
maybeSendWireHandshake()
|
|
2466
2604
|
}
|
|
2467
2605
|
if let deviceModel = fields["device_model"] as? String {
|
|
2468
2606
|
DeviceStore.shared.apply("glasses", "deviceModel", deviceModel)
|
|
@@ -2595,10 +2733,13 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2595
2733
|
if let bodyObj = json["B"] as? [String: Any] {
|
|
2596
2734
|
let readyResponse = bodyObj["ready"] as? Int ?? 0
|
|
2597
2735
|
|
|
2598
|
-
// Extract battery info from heartbeat
|
|
2736
|
+
// Extract battery info from heartbeat. charg is the PMU charging bit — the
|
|
2737
|
+
// only truthful charging source in the protocol. Old firmware omits it;
|
|
2738
|
+
// keep the last known state then instead of defaulting to not-charging.
|
|
2599
2739
|
let percentage = bodyObj["pt"] as? Int ?? 0
|
|
2600
2740
|
let voltage = bodyObj["vt"] as? Int ?? 0
|
|
2601
|
-
let
|
|
2741
|
+
let chargBit = bodyObj["charg"] as? Int
|
|
2742
|
+
let charging = chargBit != nil ? (chargBit == 1) : self.charging
|
|
2602
2743
|
|
|
2603
2744
|
// SOC is still booting
|
|
2604
2745
|
if readyResponse == 0 {
|
|
@@ -2646,12 +2787,15 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2646
2787
|
let percentage = body["pt"] as? Int
|
|
2647
2788
|
{
|
|
2648
2789
|
let voltageVolts = Double(voltage) / 1000.0
|
|
2649
|
-
let isCharging = voltage > 4000
|
|
2650
2790
|
|
|
2651
2791
|
Bridge.log(
|
|
2652
2792
|
"🔋 K900 Battery Status - Voltage: \(voltageVolts)V, Level: \(percentage)%"
|
|
2653
2793
|
)
|
|
2654
|
-
|
|
2794
|
+
// Percent only. sr_batv carries just voltage+percent; inferring charging
|
|
2795
|
+
// from voltage (>4.0V) reads "not charging" for most of a genuinely-charging
|
|
2796
|
+
// pack's range. Charging state comes exclusively from the PMU charg bit in
|
|
2797
|
+
// the sr_hrt heartbeat.
|
|
2798
|
+
updateBatteryStatus(level: percentage, isCharging: charging)
|
|
2655
2799
|
}
|
|
2656
2800
|
|
|
2657
2801
|
case "sr_getvol":
|
|
@@ -2975,6 +3119,8 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2975
3119
|
Bridge.log("LIVE: 🎉 Received glasses_ready message - SOC is booted and ready!")
|
|
2976
3120
|
|
|
2977
3121
|
stopReadinessCheckLoop()
|
|
3122
|
+
advertiseFilePayloadCapabilityToBes()
|
|
3123
|
+
openL2capFileChannel()
|
|
2978
3124
|
sendBleMtuConfig()
|
|
2979
3125
|
|
|
2980
3126
|
// Invalidate any version fields from a prior link session so the next version_info
|
|
@@ -3057,6 +3203,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3057
3203
|
DeviceStore.shared.apply("glasses", "firmwareVersion", firmwareVersion)
|
|
3058
3204
|
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", bluetoothMacAddress)
|
|
3059
3205
|
isNewVersion = (Int(buildNumber) ?? 0) >= 5
|
|
3206
|
+
maybeSendWireHandshake()
|
|
3060
3207
|
DeviceStore.shared.apply("glasses", "deviceModel", deviceModel)
|
|
3061
3208
|
DeviceStore.shared.apply("glasses", "androidVersion", androidVersion)
|
|
3062
3209
|
|
|
@@ -3290,7 +3437,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3290
3437
|
}
|
|
3291
3438
|
|
|
3292
3439
|
if let incidentRelay = bleIncidentLogRelays[bleImgId] {
|
|
3293
|
-
|
|
3440
|
+
if K900ProtocolUtils.shouldLogFilePacket(packetInfo) {
|
|
3441
|
+
Bridge.log("LIVE: 📦 BLE incident log relay packet for: \(bleImgId)")
|
|
3442
|
+
}
|
|
3294
3443
|
|
|
3295
3444
|
if incidentRelay.session == nil {
|
|
3296
3445
|
activeFileTransfers.removeValue(forKey: packetInfo.fileName)
|
|
@@ -3340,7 +3489,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3340
3489
|
|
|
3341
3490
|
if var photoTransfer = blePhotoTransfers[bleImgId] {
|
|
3342
3491
|
// This is a BLE photo transfer
|
|
3343
|
-
|
|
3492
|
+
if K900ProtocolUtils.shouldLogFilePacket(packetInfo) {
|
|
3493
|
+
Bridge.log("📦 BLE photo transfer packet for requestId: \(photoTransfer.requestId)")
|
|
3494
|
+
}
|
|
3344
3495
|
|
|
3345
3496
|
// Get or create session for this transfer
|
|
3346
3497
|
if photoTransfer.session == nil {
|
|
@@ -3436,9 +3587,11 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3436
3587
|
activeFileTransfers[packetInfo.fileName] = sess
|
|
3437
3588
|
|
|
3438
3589
|
if added {
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3590
|
+
if K900ProtocolUtils.shouldLogFilePacket(packetInfo) {
|
|
3591
|
+
Bridge.log(
|
|
3592
|
+
"LIVE: 📦 Packet \(packetInfo.packIndex) received successfully (BES will auto-ACK)"
|
|
3593
|
+
)
|
|
3594
|
+
}
|
|
3442
3595
|
|
|
3443
3596
|
if sess.isComplete {
|
|
3444
3597
|
Bridge.log("LIVE: 📦 File transfer complete: \(packetInfo.fileName)")
|
|
@@ -3566,7 +3719,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3566
3719
|
let basePath = components.path.hasSuffix("/")
|
|
3567
3720
|
? String(components.path.dropLast())
|
|
3568
3721
|
: components.path
|
|
3569
|
-
components.path = basePath + "/api/
|
|
3722
|
+
components.path = basePath + "/api/client/reports/\(relay.incidentId)/artifacts"
|
|
3570
3723
|
guard let url = components.url else {
|
|
3571
3724
|
sendTransferCompleteConfirmation(fileName: fileName, success: false)
|
|
3572
3725
|
if let existing = bleIncidentLogRelays[relay.fileBaseKey] {
|
|
@@ -3911,6 +4064,162 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3911
4064
|
}
|
|
3912
4065
|
}
|
|
3913
4066
|
|
|
4067
|
+
private func maybeSendWireHandshake() {
|
|
4068
|
+
// Only attempt the v2 binary handshake once the glasses have advertised binary support via
|
|
4069
|
+
// wire_caps. Older builds that report new version but lack wire_caps stay on the legacy path.
|
|
4070
|
+
guard isNewVersion,
|
|
4071
|
+
peerWireCapsBinary,
|
|
4072
|
+
!wireHandshakeQueued,
|
|
4073
|
+
peerWireProtocolVersion < BleWireProtocol.protocolV2
|
|
4074
|
+
else { return }
|
|
4075
|
+
sendWireHandshake()
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
private func sendWireHandshake() {
|
|
4079
|
+
guard isNewVersion else { return }
|
|
4080
|
+
var flags = BleWireProtocol.flagHandshake
|
|
4081
|
+
flags |= BleWireProtocol.flagFirstFrag
|
|
4082
|
+
flags |= BleWireProtocol.flagLastFrag
|
|
4083
|
+
let payload = Data(BleWireProtocol.handshakePayloadV2.utf8)
|
|
4084
|
+
guard let packed = BleWireProtocol.packBinaryFragment(
|
|
4085
|
+
flags: flags,
|
|
4086
|
+
msgId: 0,
|
|
4087
|
+
fragIdx: 0,
|
|
4088
|
+
fragCount: 1,
|
|
4089
|
+
payload: payload
|
|
4090
|
+
) else {
|
|
4091
|
+
return
|
|
4092
|
+
}
|
|
4093
|
+
Bridge.log("LIVE: Sending BLE wire v2 handshake")
|
|
4094
|
+
wireHandshakeQueued = true
|
|
4095
|
+
queueSend(packed, id: "-1")
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
private func activateBinaryWireV2Session(logMessage: String) {
|
|
4099
|
+
peerWireProtocolVersion = BleWireProtocol.protocolV2
|
|
4100
|
+
useBinaryWireProtocol = true
|
|
4101
|
+
wireHandshakeQueued = false
|
|
4102
|
+
peerK900Le = true
|
|
4103
|
+
BleJsonCompact.markSessionConnected(epochMs: Int64(Date().timeIntervalSince1970 * 1000))
|
|
4104
|
+
Bridge.log(logMessage)
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
private func handlePeerWireHandshake() {
|
|
4108
|
+
activateBinaryWireV2Session(logMessage: "LIVE: Peer confirmed BLE wire protocol v2")
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
private func processBinaryWireFrame(_ data: Data) {
|
|
4112
|
+
guard let info = BleWireProtocol.extractBinaryFragmentInfo(data) else {
|
|
4113
|
+
Bridge.log("LIVE: Failed to parse binary wire frame")
|
|
4114
|
+
return
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
if BleWireProtocol.isHandshakeV2(info) {
|
|
4118
|
+
handlePeerWireHandshake()
|
|
4119
|
+
return
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
if !useBinaryWireProtocol, isNewVersion {
|
|
4123
|
+
activateBinaryWireV2Session(logMessage: "LIVE: Auto-enabled BLE wire v2 from incoming binary frame")
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
guard let reassembled = incomingChunkReassembler.addBinaryFragment(
|
|
4127
|
+
msgId: info.msgId,
|
|
4128
|
+
fragIdx: Int(info.fragIdx),
|
|
4129
|
+
fragCount: Int(info.fragCount),
|
|
4130
|
+
data: info.payload
|
|
4131
|
+
) else {
|
|
4132
|
+
return
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
guard let jsonString = String(data: reassembled, encoding: .utf8) else {
|
|
4136
|
+
Bridge.log("LIVE: Failed to decode reassembled binary wire payload")
|
|
4137
|
+
return
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
logWireMetrics(
|
|
4141
|
+
payloadBytes: reassembled.count,
|
|
4142
|
+
wireBytes: data.count,
|
|
4143
|
+
packetCount: Int(info.fragCount),
|
|
4144
|
+
protocolVersion: BleWireProtocol.protocolV2,
|
|
4145
|
+
direction: "glasses_to_phone"
|
|
4146
|
+
)
|
|
4147
|
+
processJsonMessage(jsonString)
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
private func compactWireJson(_ json: [String: Any]) -> [String: Any] {
|
|
4151
|
+
guard useBinaryWireProtocol, isNewVersion else { return json }
|
|
4152
|
+
return BleJsonCompact.encode(json)
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
private func expandCompactWireJson(_ json: [String: Any]) -> [String: Any]? {
|
|
4156
|
+
guard useBinaryWireProtocol, isNewVersion else { return json }
|
|
4157
|
+
return BleJsonCompact.decodeIfSupported(json)
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
private func logWireMetrics(
|
|
4161
|
+
payloadBytes: Int,
|
|
4162
|
+
wireBytes: Int,
|
|
4163
|
+
packetCount: Int,
|
|
4164
|
+
protocolVersion: Int,
|
|
4165
|
+
direction: String
|
|
4166
|
+
) {
|
|
4167
|
+
Bridge.log(
|
|
4168
|
+
"BLE_TRACE direction=\(direction) proto=v\(protocolVersion) payload=\(payloadBytes) wire=\(wireBytes) packets=\(packetCount)"
|
|
4169
|
+
)
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
private func sendJsonBinary(
|
|
4173
|
+
jsonString: String,
|
|
4174
|
+
messageId: Int64,
|
|
4175
|
+
trackingId: String,
|
|
4176
|
+
wakeUp: Bool,
|
|
4177
|
+
requireAck: Bool
|
|
4178
|
+
) {
|
|
4179
|
+
let payload = Data(jsonString.utf8)
|
|
4180
|
+
let msgId = UInt16(truncatingIfNeeded: messageId)
|
|
4181
|
+
let fragments = MessageChunker.createBinaryFragments(
|
|
4182
|
+
payload: payload,
|
|
4183
|
+
msgId: msgId,
|
|
4184
|
+
wakeUp: wakeUp,
|
|
4185
|
+
ackRequested: requireAck && messageId >= 0
|
|
4186
|
+
)
|
|
4187
|
+
guard !fragments.isEmpty else {
|
|
4188
|
+
Bridge.log("LIVE: Failed to create binary wire fragments")
|
|
4189
|
+
return
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
var totalWireBytes = 0
|
|
4193
|
+
for (index, fragment) in fragments.enumerated() {
|
|
4194
|
+
guard let packed = BleWireProtocol.packBinaryFragment(
|
|
4195
|
+
flags: fragment.flags,
|
|
4196
|
+
msgId: fragment.msgId,
|
|
4197
|
+
fragIdx: fragment.fragIdx,
|
|
4198
|
+
fragCount: fragment.fragCount,
|
|
4199
|
+
payload: fragment.payload
|
|
4200
|
+
) else {
|
|
4201
|
+
continue
|
|
4202
|
+
}
|
|
4203
|
+
totalWireBytes += packed.count
|
|
4204
|
+
let isFinalChunk = index == fragments.count - 1
|
|
4205
|
+
let chunkTrackingId = (requireAck && isFinalChunk) ? trackingId : "-1"
|
|
4206
|
+
queueSend(packed, id: chunkTrackingId)
|
|
4207
|
+
|
|
4208
|
+
if index < fragments.count - 1 {
|
|
4209
|
+
Thread.sleep(forTimeInterval: 0.05)
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
logWireMetrics(
|
|
4214
|
+
payloadBytes: payload.count,
|
|
4215
|
+
wireBytes: totalWireBytes,
|
|
4216
|
+
packetCount: fragments.count,
|
|
4217
|
+
protocolVersion: BleWireProtocol.protocolV2,
|
|
4218
|
+
direction: "phone_to_glasses"
|
|
4219
|
+
)
|
|
4220
|
+
Bridge.log("LIVE: Binary v2 queued \(fragments.count) fragments, wireBytes=\(totalWireBytes)")
|
|
4221
|
+
}
|
|
4222
|
+
|
|
3914
4223
|
func sendJson(_ jsonOriginal: [String: Any], wakeUp: Bool = false, requireAck: Bool = true) {
|
|
3915
4224
|
do {
|
|
3916
4225
|
var json = jsonOriginal
|
|
@@ -3924,7 +4233,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3924
4233
|
globalMessageId += 1
|
|
3925
4234
|
}
|
|
3926
4235
|
|
|
3927
|
-
let jsonData = try JSONSerialization.data(withJSONObject: json)
|
|
4236
|
+
let jsonData = try JSONSerialization.data(withJSONObject: compactWireJson(json))
|
|
3928
4237
|
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
3929
4238
|
let commandInfo = outgoingBleCommandTraceInfo(json)
|
|
3930
4239
|
BleTraceLogger.logJson(
|
|
@@ -3934,6 +4243,17 @@ class MentraLive: NSObject, SGCManager {
|
|
|
3934
4243
|
bytes: jsonData.count
|
|
3935
4244
|
)
|
|
3936
4245
|
|
|
4246
|
+
if useBinaryWireProtocol, isNewVersion {
|
|
4247
|
+
sendJsonBinary(
|
|
4248
|
+
jsonString: jsonString,
|
|
4249
|
+
messageId: messageId,
|
|
4250
|
+
trackingId: trackingId,
|
|
4251
|
+
wakeUp: wakeUp,
|
|
4252
|
+
requireAck: requireAck
|
|
4253
|
+
)
|
|
4254
|
+
return
|
|
4255
|
+
}
|
|
4256
|
+
|
|
3937
4257
|
// First check if the message needs chunking
|
|
3938
4258
|
// Create a test C-wrapped version to check size
|
|
3939
4259
|
var testWrapper: [String: Any] = [K900ProtocolUtils.FIELD_C: jsonString]
|
|
@@ -4687,6 +5007,14 @@ class MentraLive: NSObject, SGCManager {
|
|
|
4687
5007
|
// Stop all timers
|
|
4688
5008
|
stopAllTimers()
|
|
4689
5009
|
incomingChunkReassembler.clear()
|
|
5010
|
+
peerWireProtocolVersion = 0
|
|
5011
|
+
useBinaryWireProtocol = false
|
|
5012
|
+
wireHandshakeQueued = false
|
|
5013
|
+
peerK900Le = false
|
|
5014
|
+
peerWireCapsBinary = false
|
|
5015
|
+
peerFilePayloadV2 = false
|
|
5016
|
+
BleJsonCompact.resetSession()
|
|
5017
|
+
closeL2capFileChannel()
|
|
4690
5018
|
|
|
4691
5019
|
// Disconnect BLE
|
|
4692
5020
|
if let peripheral = connectedPeripheral {
|
|
@@ -4745,12 +5073,72 @@ extension MentraLive {
|
|
|
4745
5073
|
return result
|
|
4746
5074
|
}
|
|
4747
5075
|
|
|
5076
|
+
/**
|
|
5077
|
+
* Heuristically detect the K900 STRING length field's endianness for a received frame. Mirrors
|
|
5078
|
+
* the firmware and Android codec: a length is plausible when it is non-zero, within the 512-byte
|
|
5079
|
+
* cap, and the full framed command fits inside the buffer; when both fit, prefer the one landing
|
|
5080
|
+
* on the trailing `$$`, then the smaller. Returns (length, isLittleEndian) or nil.
|
|
5081
|
+
*/
|
|
5082
|
+
private func k900DetectStringLength(_ bytes: [UInt8]) -> (length: Int, isLe: Bool)? {
|
|
5083
|
+
guard bytes.count >= 7 else { return nil }
|
|
5084
|
+
let leLen = Int(bytes[3]) | (Int(bytes[4]) << 8)
|
|
5085
|
+
let beLen = (Int(bytes[3]) << 8) | Int(bytes[4])
|
|
5086
|
+
|
|
5087
|
+
let leOk = leLen > 0 && leLen <= 512 && leLen + 7 <= bytes.count
|
|
5088
|
+
let beOk = beLen > 0 && beLen <= 512 && beLen + 7 <= bytes.count
|
|
5089
|
+
|
|
5090
|
+
func endsWithEndCode(_ end: Int) -> Bool {
|
|
5091
|
+
end + 1 < bytes.count && bytes[end] == 0x24 && bytes[end + 1] == 0x24
|
|
5092
|
+
}
|
|
5093
|
+
|
|
5094
|
+
if leOk, beOk {
|
|
5095
|
+
if endsWithEndCode(5 + leLen) { return (leLen, true) }
|
|
5096
|
+
if endsWithEndCode(5 + beLen) { return (beLen, false) }
|
|
5097
|
+
return leLen <= beLen ? (leLen, true) : (beLen, false)
|
|
5098
|
+
}
|
|
5099
|
+
if leOk { return (leLen, true) }
|
|
5100
|
+
if beOk { return (beLen, false) }
|
|
5101
|
+
return nil
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5104
|
+
/**
|
|
5105
|
+
* Parse a `wire_caps` object from a version_info/glasses_ready message and update the negotiated
|
|
5106
|
+
* per-link endianness and binary support. Missing wire_caps leaves the legacy defaults (BE, no
|
|
5107
|
+
* binary) untouched so older glasses keep working.
|
|
5108
|
+
*/
|
|
5109
|
+
private func parsePeerWireCaps(_ json: [String: Any]) {
|
|
5110
|
+
guard let caps = json["wire_caps"] as? [String: Any] else { return }
|
|
5111
|
+
if (caps["k900_le"] as? Bool) == true {
|
|
5112
|
+
if !peerK900Le {
|
|
5113
|
+
peerK900Le = true
|
|
5114
|
+
Bridge.log("LIVE: wire_caps negotiated k900 endian=LE")
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
if caps.keys.contains("binary") {
|
|
5118
|
+
peerWireCapsBinary = (caps["binary"] as? Bool) == true
|
|
5119
|
+
}
|
|
5120
|
+
if caps.keys.contains("file_payload_v2") {
|
|
5121
|
+
peerFilePayloadV2 = (caps["file_payload_v2"] as? Bool) == true
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5125
|
+
private func advertiseFilePayloadCapabilityToBes() {
|
|
5126
|
+
guard peerFilePayloadV2 else { return }
|
|
5127
|
+
let body = "{\"max\":\(K900ProtocolUtils.FILE_PACK_SIZE)}"
|
|
5128
|
+
let command: [String: Any] = ["C": "cs_file_payload", "B": body]
|
|
5129
|
+
if sendRawK900Command(command) {
|
|
5130
|
+
Bridge.log(
|
|
5131
|
+
"LIVE: 📦 Advertised file payload ceiling \(K900ProtocolUtils.FILE_PACK_SIZE) to BES"
|
|
5132
|
+
)
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
|
|
4748
5136
|
/**
|
|
4749
5137
|
* Pack raw byte data with K900 BES2700 protocol format for phone-to-device communication
|
|
4750
5138
|
* Format: ## + command_type + length(2bytes) + data + $$
|
|
4751
5139
|
* Uses little-endian byte order for length field
|
|
4752
5140
|
*/
|
|
4753
|
-
private func packDataToK900(_ data: Data?, cmdType: UInt8) -> Data? {
|
|
5141
|
+
private func packDataToK900(_ data: Data?, cmdType: UInt8, littleEndian: Bool = false) -> Data? {
|
|
4754
5142
|
guard let data else { return nil }
|
|
4755
5143
|
|
|
4756
5144
|
let dataLength = data.count
|
|
@@ -4764,9 +5152,14 @@ extension MentraLive {
|
|
|
4764
5152
|
// Command type
|
|
4765
5153
|
result.append(cmdType)
|
|
4766
5154
|
|
|
4767
|
-
// Length (2 bytes,
|
|
4768
|
-
|
|
4769
|
-
|
|
5155
|
+
// Length (2 bytes, negotiated endianness)
|
|
5156
|
+
if littleEndian {
|
|
5157
|
+
result.append(UInt8(dataLength & 0xFF)) // LSB first
|
|
5158
|
+
result.append(UInt8((dataLength >> 8) & 0xFF)) // MSB second
|
|
5159
|
+
} else {
|
|
5160
|
+
result.append(UInt8((dataLength >> 8) & 0xFF)) // MSB first
|
|
5161
|
+
result.append(UInt8(dataLength & 0xFF)) // LSB second
|
|
5162
|
+
}
|
|
4770
5163
|
|
|
4771
5164
|
// Copy the data
|
|
4772
5165
|
result.append(data)
|
|
@@ -4796,9 +5189,13 @@ extension MentraLive {
|
|
|
4796
5189
|
let jsonData = try JSONSerialization.data(withJSONObject: wrapper)
|
|
4797
5190
|
guard let wrappedJson = String(data: jsonData, encoding: .utf8) else { return nil }
|
|
4798
5191
|
|
|
4799
|
-
// Then pack with BES2700 protocol format using
|
|
5192
|
+
// Then pack with BES2700 protocol format using the negotiated endianness
|
|
4800
5193
|
let jsonBytes = wrappedJson.data(using: .utf8)!
|
|
4801
|
-
return packDataToK900(
|
|
5194
|
+
return packDataToK900(
|
|
5195
|
+
jsonBytes,
|
|
5196
|
+
cmdType: K900ProtocolUtils.CMD_TYPE_STRING,
|
|
5197
|
+
littleEndian: peerK900Le
|
|
5198
|
+
)
|
|
4802
5199
|
|
|
4803
5200
|
} catch {
|
|
4804
5201
|
Bridge.log("Error creating JSON wrapper for K900: \(error)")
|
|
@@ -5091,7 +5488,11 @@ extension MentraLive {
|
|
|
5091
5488
|
}
|
|
5092
5489
|
let commandData = try JSONSerialization.data(withJSONObject: payload)
|
|
5093
5490
|
guard
|
|
5094
|
-
let packet = packDataToK900(
|
|
5491
|
+
let packet = packDataToK900(
|
|
5492
|
+
commandData,
|
|
5493
|
+
cmdType: K900ProtocolUtils.CMD_TYPE_STRING,
|
|
5494
|
+
littleEndian: peerK900Le
|
|
5495
|
+
)
|
|
5095
5496
|
else {
|
|
5096
5497
|
Bridge.log("LIVE: Failed to pack raw K900 command")
|
|
5097
5498
|
return false
|