@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.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +52 -4
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +119 -16
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +0 -6
  5. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +22 -21
  6. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
  7. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
  8. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +457 -31
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +361 -67
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +7501 -3704
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +134 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
  14. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +4 -1
  16. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleWireProtocol.java +108 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +4 -3
  18. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900LengthCodec.java +115 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +103 -68
  20. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +106 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +93 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +111 -11
  23. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +7 -9
  24. package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +17 -1
  25. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
  26. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +158 -0
  27. package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
  28. package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
  29. package/build/BluetoothSdk.types.d.ts +12 -1
  30. package/build/BluetoothSdk.types.d.ts.map +1 -1
  31. package/build/BluetoothSdk.types.js.map +1 -1
  32. package/build/types/index.d.ts +3 -0
  33. package/build/types/index.d.ts.map +1 -0
  34. package/build/types/index.js +2 -0
  35. package/build/types/index.js.map +1 -0
  36. package/ios/LocalPhotoUploadServer.swift +78 -0
  37. package/ios/MentraPhotoReceiverModule.swift +10 -0
  38. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  39. package/ios/Source/Bridge.swift +63 -0
  40. package/ios/Source/DeviceManager.swift +134 -29
  41. package/ios/Source/DeviceStore.swift +0 -5
  42. package/ios/Source/MentraBluetoothSDK.swift +26 -23
  43. package/ios/Source/OtaManifest.swift +1 -1
  44. package/ios/Source/internal/BleTraceLogger.swift +192 -0
  45. package/ios/Source/sgcs/G2.swift +518 -11
  46. package/ios/Source/sgcs/MentraLive.swift +607 -39
  47. package/ios/Source/sgcs/MentraNex.swift +416 -32
  48. package/ios/Source/sgcs/SGCManager.swift +155 -0
  49. package/ios/Source/sgcs/mentraos_ble.pb.swift +726 -122
  50. package/ios/Source/status/DeviceStatus.swift +0 -8
  51. package/ios/Source/utils/BleJsonCompact.swift +395 -0
  52. package/ios/Source/utils/BleWireProtocol.swift +92 -0
  53. package/ios/Source/utils/MessageChunkReassembler.swift +82 -0
  54. package/ios/Source/utils/MessageChunker.swift +73 -0
  55. package/package.json +14 -7
  56. package/src/BluetoothSdk.types.ts +13 -1
  57. 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
- guard var jpegData = image.jpegData(compressionQuality: 0.9) else {
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 = 256
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
- let payloadLength: Int
2127
+ if commandType == K900ProtocolUtils.CMD_TYPE_BINARY_MSG {
2128
+ processBinaryWireFrame(data)
2129
+ return
2130
+ }
2059
2131
 
2060
- // Determine endianness based on device name
2061
- if let deviceName = connectedPeripheral?.name,
2062
- deviceName.hasPrefix("XyBLE_") || deviceName.lowercased().hasPrefix("mentra_live")
2063
- {
2064
- // K900 device - big-endian
2065
- payloadLength = (Int(bytes[3]) << 8) | Int(bytes[4])
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(_ json: [String: Any]) {
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
- let level = json["level"] as? Int ?? batteryLevel
2154
- let isCharging = json["charging"] as? Bool ?? charging
2155
- updateBatteryStatus(level: level, isCharging: isCharging)
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 charging = (bodyObj["charg"] as? Int ?? 0) == 1
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
- updateBatteryStatus(level: percentage, isCharging: isCharging)
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/incidents/\(relay.incidentId)/logs"
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
- queueSend(packedData, id: chunkTrackingId)
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
- queueSend(packedData, id: trackingId)
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, little-endian for phone-to-device)
4478
- result.append(UInt8(dataLength & 0xFF)) // LSB first
4479
- result.append(UInt8((dataLength >> 8) & 0xFF)) // MSB second
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 little-endian
5069
+ // Then pack with BES2700 protocol format using the negotiated endianness
4510
5070
  let jsonBytes = wrappedJson.data(using: .utf8)!
4511
- return packDataToK900(jsonBytes, cmdType: K900ProtocolUtils.CMD_TYPE_STRING)
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(commandData, cmdType: K900ProtocolUtils.CMD_TYPE_STRING)
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