@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
@@ -37,7 +37,10 @@ import org.json.JSONObject;
37
37
  */
38
38
  public class BlePhotoUploadService {
39
39
  private static final String TAG = "BlePhotoUploadService";
40
- private static final int JPEG_QUALITY = 90;
40
+ // 100: the source already went through a lossy AVIF pass on the glasses, so
41
+ // this re-encode must not compound the loss. Phone CPU and upload bandwidth
42
+ // are cheap relative to what was paid to get the bytes over BLE.
43
+ private static final int JPEG_QUALITY = 100;
41
44
 
42
45
  public interface UploadCallback {
43
46
  void onSuccess(String requestId, String responseBody);
@@ -0,0 +1,108 @@
1
+ package com.mentra.bluetoothsdk.utils;
2
+
3
+ import java.nio.charset.StandardCharsets;
4
+
5
+ /**
6
+ * BLE Wire Protocol v2 shared constants and binary frame helpers.
7
+ * All multi-byte fields use little-endian byte order.
8
+ */
9
+ public final class BleWireProtocol {
10
+
11
+ private BleWireProtocol() {}
12
+
13
+ public static final byte CMD_TYPE_BINARY_MSG = 0x40;
14
+
15
+ public static final byte BLE_WIRE_FLAG_FIRST_FRAG = 0x01;
16
+ public static final byte BLE_WIRE_FLAG_LAST_FRAG = 0x02;
17
+ public static final byte BLE_WIRE_FLAG_WAKE = 0x04;
18
+ public static final byte BLE_WIRE_FLAG_HANDSHAKE = 0x08;
19
+ public static final byte BLE_WIRE_FLAG_ACK_REQUESTED = 0x10;
20
+
21
+ public static final int BINARY_HEADER_SIZE = 7;
22
+ public static final int MAX_FRAGMENT_PAYLOAD = 480;
23
+ public static final int MTU_TARGET = 509;
24
+
25
+ public static final String HANDSHAKE_PAYLOAD_V2 = "v2";
26
+ public static final int PROTOCOL_V1 = 1;
27
+ public static final int PROTOCOL_V2 = 2;
28
+
29
+ public static class BinaryFragmentInfo {
30
+ public byte flags;
31
+ public int msgId;
32
+ public int fragIdx;
33
+ public int fragCount;
34
+ public byte[] payload;
35
+ }
36
+
37
+ public static boolean isBinaryFrame(byte[] data) {
38
+ return K900ProtocolUtils.isK900ProtocolFormat(data)
39
+ && data.length >= 7
40
+ && data[2] == CMD_TYPE_BINARY_MSG;
41
+ }
42
+
43
+ public static byte[] packBinaryFragment(
44
+ byte flags, int msgId, int fragIdx, int fragCount, byte[] payload) {
45
+ int payloadLen = payload != null ? payload.length : 0;
46
+ byte[] inner = new byte[BINARY_HEADER_SIZE + payloadLen];
47
+ inner[0] = flags;
48
+ writeLe16(inner, 1, msgId);
49
+ inner[3] = (byte) fragIdx;
50
+ inner[4] = (byte) fragCount;
51
+ writeLe16(inner, 5, payloadLen);
52
+ if (payloadLen > 0) {
53
+ System.arraycopy(payload, 0, inner, BINARY_HEADER_SIZE, payloadLen);
54
+ }
55
+ return K900ProtocolUtils.packDataToK900(
56
+ inner, CMD_TYPE_BINARY_MSG, K900LengthCodec.Endian.LE);
57
+ }
58
+
59
+ public static BinaryFragmentInfo extractBinaryFragmentInfo(byte[] frame) {
60
+ if (!isBinaryFrame(frame)) {
61
+ return null;
62
+ }
63
+
64
+ int innerLen = readLe16(frame, 3);
65
+ if (innerLen < BINARY_HEADER_SIZE || frame.length < 7 + innerLen) {
66
+ return null;
67
+ }
68
+
69
+ int offset = 5;
70
+ int payloadLen = readLe16(frame, offset + 5);
71
+ if (BINARY_HEADER_SIZE + payloadLen > innerLen) {
72
+ return null;
73
+ }
74
+
75
+ BinaryFragmentInfo info = new BinaryFragmentInfo();
76
+ info.flags = frame[offset];
77
+ info.msgId = readLe16(frame, offset + 1);
78
+ info.fragIdx = frame[offset + 3] & 0xFF;
79
+ info.fragCount = frame[offset + 4] & 0xFF;
80
+ info.payload = new byte[payloadLen];
81
+ if (payloadLen > 0) {
82
+ System.arraycopy(frame, offset + BINARY_HEADER_SIZE, info.payload, 0, payloadLen);
83
+ }
84
+ return info;
85
+ }
86
+
87
+ public static byte[] extractBinaryPayload(byte[] frame) {
88
+ BinaryFragmentInfo info = extractBinaryFragmentInfo(frame);
89
+ return info != null ? info.payload : null;
90
+ }
91
+
92
+ public static boolean isHandshakeV2(BinaryFragmentInfo info) {
93
+ if (info == null || (info.flags & BLE_WIRE_FLAG_HANDSHAKE) == 0) {
94
+ return false;
95
+ }
96
+ String payload = new String(info.payload, StandardCharsets.UTF_8);
97
+ return HANDSHAKE_PAYLOAD_V2.equals(payload);
98
+ }
99
+
100
+ static int readLe16(byte[] data, int offset) {
101
+ return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
102
+ }
103
+
104
+ static void writeLe16(byte[] data, int offset, int value) {
105
+ data[offset] = (byte) (value & 0xFF);
106
+ data[offset + 1] = (byte) ((value >> 8) & 0xFF);
107
+ }
108
+ }
@@ -13,7 +13,7 @@ import okhttp3.RequestBody;
13
13
  import okhttp3.Response;
14
14
 
15
15
  /**
16
- * POSTs incident log JSON (from BLE relay) using the same path as glasses direct upload.
16
+ * POSTs report artifact JSON from the BLE relay using the same path as glasses direct upload.
17
17
  */
18
18
  public final class IncidentLogBleUploadService {
19
19
 
@@ -42,9 +42,10 @@ public final class IncidentLogBleUploadService {
42
42
  }
43
43
  HttpUrl url = baseHttpUrl.newBuilder()
44
44
  .addPathSegment("api")
45
- .addPathSegment("incidents")
45
+ .addPathSegment("client")
46
+ .addPathSegment("reports")
46
47
  .addPathSegment(incidentId)
47
- .addPathSegment("logs")
48
+ .addPathSegment("artifacts")
48
49
  .build();
49
50
  RequestBody body = RequestBody.create(jsonUtf8, JSON);
50
51
  OkHttpClient client = new OkHttpClient.Builder()
@@ -0,0 +1,115 @@
1
+ package com.mentra.bluetoothsdk.utils;
2
+
3
+ /**
4
+ * Encodes and decodes the 2-byte length field of K900 {@code ##} STRING (0x30) frames.
5
+ *
6
+ * <p>Legacy BES firmware uses big-endian for this field; BLE Wire v2 clients use little-endian.
7
+ * The two conventions are negotiated per link (see {@code wire_caps}); until a peer advertises
8
+ * little-endian, callers should default to {@link Endian#BE}. On receive, {@link #detectLength}
9
+ * heuristically recovers the endianness so we interoperate with peers of unknown vintage.
10
+ *
11
+ * <p>File-transfer header fields (packSize/packIndex/fileSize) are always big-endian and are not
12
+ * handled here.
13
+ *
14
+ * <p>Mirror of the ASG-side {@code K900LengthCodec} and firmware {@code k900_detect_string_length}.
15
+ */
16
+ public final class K900LengthCodec {
17
+
18
+ private K900LengthCodec() {}
19
+
20
+ public enum Endian {
21
+ BE,
22
+ LE
23
+ }
24
+
25
+ public static final int FRAME_OVERHEAD = 7;
26
+ public static final int LENGTH_OFFSET = 3;
27
+ public static final int PAYLOAD_OFFSET = 5;
28
+ public static final int MAX_STRING_LENGTH = 512;
29
+
30
+ public static int readLength(byte[] frame, int offset, Endian endian) {
31
+ int b0 = frame[offset] & 0xFF;
32
+ int b1 = frame[offset + 1] & 0xFF;
33
+ return endian == Endian.LE ? (b0 | (b1 << 8)) : ((b0 << 8) | b1);
34
+ }
35
+
36
+ public static void writeLength(byte[] frame, int offset, int value, Endian endian) {
37
+ if (endian == Endian.LE) {
38
+ frame[offset] = (byte) (value & 0xFF);
39
+ frame[offset + 1] = (byte) ((value >> 8) & 0xFF);
40
+ } else {
41
+ frame[offset] = (byte) ((value >> 8) & 0xFF);
42
+ frame[offset + 1] = (byte) (value & 0xFF);
43
+ }
44
+ }
45
+
46
+ public static final class Detected {
47
+ public final int length;
48
+ public final Endian endian;
49
+
50
+ Detected(int length, Endian endian) {
51
+ this.length = length;
52
+ this.endian = endian;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Heuristically determine the STRING length field's endianness for a received frame. A length is
58
+ * plausible when it is non-zero, within the cap, and the full framed command fits inside the
59
+ * buffer. When both interpretations fit, prefer the one whose payload lands exactly on the
60
+ * trailing {@code $$}, then the smaller.
61
+ *
62
+ * @return the detected length + endianness, or {@code null} if neither interpretation is valid
63
+ */
64
+ public static Detected detectLength(byte[] frame) {
65
+ if (frame == null || frame.length < FRAME_OVERHEAD) {
66
+ return null;
67
+ }
68
+
69
+ int leLen = readLength(frame, LENGTH_OFFSET, Endian.LE);
70
+ int beLen = readLength(frame, LENGTH_OFFSET, Endian.BE);
71
+
72
+ boolean leOk = leLen > 0 && leLen <= MAX_STRING_LENGTH && leLen + FRAME_OVERHEAD <= frame.length;
73
+ boolean beOk = beLen > 0 && beLen <= MAX_STRING_LENGTH && beLen + FRAME_OVERHEAD <= frame.length;
74
+
75
+ if (leOk && beOk) {
76
+ if (endsWithEndCode(frame, PAYLOAD_OFFSET + leLen)) {
77
+ return new Detected(leLen, Endian.LE);
78
+ }
79
+ if (endsWithEndCode(frame, PAYLOAD_OFFSET + beLen)) {
80
+ return new Detected(beLen, Endian.BE);
81
+ }
82
+ return leLen <= beLen ? new Detected(leLen, Endian.LE) : new Detected(beLen, Endian.BE);
83
+ }
84
+ if (leOk) {
85
+ return new Detected(leLen, Endian.LE);
86
+ }
87
+ if (beOk) {
88
+ return new Detected(beLen, Endian.BE);
89
+ }
90
+ return null;
91
+ }
92
+
93
+ private static boolean endsWithEndCode(byte[] frame, int endOffset) {
94
+ return endOffset + 1 < frame.length
95
+ && frame[endOffset] == K900ProtocolUtils.CMD_END_CODE[0]
96
+ && frame[endOffset + 1] == K900ProtocolUtils.CMD_END_CODE[1];
97
+ }
98
+
99
+ /**
100
+ * Transcode a K900 STRING frame's length bytes from one endianness to another, leaving the
101
+ * payload untouched. Returns a new array; the input is not modified.
102
+ */
103
+ public static byte[] repackStringFrame(byte[] frame, Endian from, Endian to) {
104
+ if (frame == null || frame.length < FRAME_OVERHEAD) {
105
+ return frame;
106
+ }
107
+ if (from == to) {
108
+ return frame.clone();
109
+ }
110
+ byte[] out = frame.clone();
111
+ int length = readLength(frame, LENGTH_OFFSET, from);
112
+ writeLength(out, LENGTH_OFFSET, length, to);
113
+ return out;
114
+ }
115
+ }
@@ -27,6 +27,7 @@ public class K900ProtocolUtils {
27
27
  public static final byte CMD_TYPE_MUSIC = 0x33; // Music file type
28
28
  public static final byte CMD_TYPE_AUDIO = 0x34; // Audio file type
29
29
  public static final byte CMD_TYPE_DATA = 0x35; // Generic data type
30
+ public static final byte CMD_TYPE_BINARY_MSG = BleWireProtocol.CMD_TYPE_BINARY_MSG;
30
31
 
31
32
  // File transfer constants
32
33
  public static final int FILE_PACK_SIZE = 400; // Max data size per packet
@@ -54,6 +55,10 @@ public class K900ProtocolUtils {
54
55
  * @return Byte array with packed data according to protocol format
55
56
  */
56
57
  public static byte[] packJsonCommand(String jsonData) {
58
+ return packJsonCommand(jsonData, K900LengthCodec.Endian.BE);
59
+ }
60
+
61
+ public static byte[] packJsonCommand(String jsonData, K900LengthCodec.Endian endian) {
57
62
  if (jsonData == null) {
58
63
  return null;
59
64
  }
@@ -68,7 +73,7 @@ public class K900ProtocolUtils {
68
73
 
69
74
  // Then pack with BES2700 protocol format
70
75
  byte[] jsonBytes = wrappedJson.getBytes(StandardCharsets.UTF_8);
71
- return packDataCommand(jsonBytes, CMD_TYPE_STRING);
76
+ return packDataCommand(jsonBytes, CMD_TYPE_STRING, endian);
72
77
 
73
78
  } catch (JSONException e) {
74
79
  android.util.Log.e("K900ProtocolUtils", "Error creating JSON wrapper", e);
@@ -85,6 +90,16 @@ public class K900ProtocolUtils {
85
90
  * @return Byte array with packed data according to protocol format
86
91
  */
87
92
  public static byte[] packDataCommand(byte[] data, byte cmdType) {
93
+ // Default to legacy big-endian until wire_caps negotiates little-endian.
94
+ return packDataCommand(data, cmdType, K900LengthCodec.Endian.BE);
95
+ }
96
+
97
+ /**
98
+ * Pack raw byte data with K900 BES2700 protocol format, writing the 2-byte length field with an
99
+ * explicit endianness. Use the negotiated per-link endianness so both legacy big-endian and
100
+ * wire-v2 little-endian peers can parse the frame.
101
+ */
102
+ public static byte[] packDataCommand(byte[] data, byte cmdType, K900LengthCodec.Endian endian) {
88
103
  if (data == null) {
89
104
  return null;
90
105
  }
@@ -101,13 +116,8 @@ public class K900ProtocolUtils {
101
116
  // Command type
102
117
  result[2] = cmdType;
103
118
 
104
- // Length (2 bytes, big-endian)
105
- result[3] = (byte)((dataLength >> 8) & 0xFF); // MSB first
106
- result[4] = (byte)(dataLength & 0xFF); // LSB second
107
-
108
- // Original little-endian implementation (commented out)
109
- // result[3] = (byte)(dataLength & 0xFF); // LSB first
110
- // result[4] = (byte)((dataLength >> 8) & 0xFF); // MSB second
119
+ // Length (2 bytes, negotiated endianness)
120
+ K900LengthCodec.writeLength(result, 3, dataLength, endian);
111
121
 
112
122
  // Copy the data
113
123
  System.arraycopy(data, 0, result, 5, dataLength);
@@ -129,34 +139,15 @@ public class K900ProtocolUtils {
129
139
  * @return Byte array with packed data according to protocol format
130
140
  */
131
141
  public static byte[] packDataToK900(byte[] data, byte cmdType) {
132
- if (data == null) {
133
- return null;
134
- }
135
-
136
- int dataLength = data.length;
137
-
138
- // Command structure: ## + type + length(2 bytes) + data + $$
139
- byte[] result = new byte[dataLength + 7]; // 2(start) + 1(type) + 2(length) + data + 2(end)
140
-
141
- // Start code ##
142
- result[0] = CMD_START_CODE[0]; // #
143
- result[1] = CMD_START_CODE[1]; // #
144
-
145
- // Command type
146
- result[2] = cmdType;
147
-
148
- // Length (2 bytes, little-endian for phone-to-device)
149
- result[3] = (byte) (dataLength & 0xFF); // LSB first
150
- result[4] = (byte) ((dataLength >> 8) & 0xFF); // MSB second
151
-
152
- // Copy the data
153
- System.arraycopy(data, 0, result, 5, dataLength);
154
-
155
- // End code $$
156
- result[5 + dataLength] = CMD_END_CODE[0]; // $
157
- result[6 + dataLength] = CMD_END_CODE[1]; // $
142
+ return packDataToK900(data, cmdType, K900LengthCodec.Endian.BE);
143
+ }
158
144
 
159
- return result;
145
+ /**
146
+ * Pack raw byte data with K900 BES2700 protocol format for phone-to-device communication.
147
+ * Format: ## + command_type + length(2bytes) + data + $$
148
+ */
149
+ public static byte[] packDataToK900(byte[] data, byte cmdType, K900LengthCodec.Endian endian) {
150
+ return packDataCommand(data, cmdType, endian);
160
151
  }
161
152
 
162
153
  /**
@@ -168,6 +159,15 @@ public class K900ProtocolUtils {
168
159
  * @return Byte array with packed data according to protocol format
169
160
  */
170
161
  public static byte[] packJsonToK900(String jsonData, boolean wakeup) {
162
+ return packJsonToK900(jsonData, wakeup, K900LengthCodec.Endian.BE);
163
+ }
164
+
165
+ /**
166
+ * Pack a C-wrapped JSON string for phone-to-glasses transmission, writing the STRING frame
167
+ * length with the negotiated per-link endianness. Legacy big-endian glasses require BE until
168
+ * they advertise {@code wire_caps.k900_le}.
169
+ */
170
+ public static byte[] packJsonToK900(String jsonData, boolean wakeup, K900LengthCodec.Endian endian) {
171
171
  if (jsonData == null) {
172
172
  return null;
173
173
  }
@@ -183,9 +183,9 @@ public class K900ProtocolUtils {
183
183
  // Convert to string
184
184
  String wrappedJson = wrapper.toString();
185
185
 
186
- // Then pack with BES2700 protocol format using little-endian
186
+ // Then pack with BES2700 protocol format using the negotiated endianness
187
187
  byte[] jsonBytes = wrappedJson.getBytes(StandardCharsets.UTF_8);
188
- return packDataToK900(jsonBytes, CMD_TYPE_STRING);
188
+ return packDataCommand(jsonBytes, CMD_TYPE_STRING, endian);
189
189
 
190
190
  } catch (JSONException e) {
191
191
  android.util.Log.e("K900ProtocolUtils", "Error creating JSON wrapper for K900", e);
@@ -203,6 +203,14 @@ public class K900ProtocolUtils {
203
203
  * @return Formatted bytes ready for transmission
204
204
  */
205
205
  public static byte[] formatMessageForTransmission(String jsonData) {
206
+ return formatMessageForTransmission(jsonData, K900LengthCodec.Endian.BE);
207
+ }
208
+
209
+ /**
210
+ * Format an ASG-client JSON message for transmission, writing the STRING frame length with the
211
+ * negotiated per-link endianness.
212
+ */
213
+ public static byte[] formatMessageForTransmission(String jsonData, K900LengthCodec.Endian endian) {
206
214
  try {
207
215
  android.util.Log.e("K900ProtocolUtils", "🔄 Formatting message: " + jsonData);
208
216
 
@@ -218,7 +226,9 @@ public class K900ProtocolUtils {
218
226
  android.util.Log.e("K900ProtocolUtils", "🔄 After C-wrapping: " + wrappedJson);
219
227
 
220
228
  // Now format with BES2700 protocol
221
- byte[] result = packDataCommand(wrappedJson.getBytes(StandardCharsets.UTF_8), CMD_TYPE_STRING);
229
+ byte[] result =
230
+ packDataCommand(
231
+ wrappedJson.getBytes(StandardCharsets.UTF_8), CMD_TYPE_STRING, endian);
222
232
 
223
233
  // Log some bytes for debugging
224
234
  StringBuilder hexDump = new StringBuilder();
@@ -233,7 +243,7 @@ public class K900ProtocolUtils {
233
243
  } catch (JSONException e) {
234
244
  android.util.Log.e("K900ProtocolUtils", "❌ Error in formatMessageForTransmission", e);
235
245
  // Fallback: if json is invalid, still try to pack it without validation
236
- return packJsonCommand(jsonData);
246
+ return packJsonCommand(jsonData, endian);
237
247
  }
238
248
  }
239
249
 
@@ -304,56 +314,59 @@ public class K900ProtocolUtils {
304
314
  * @return Raw payload data or null if format is invalid
305
315
  */
306
316
  public static byte[] extractPayload(byte[] protocolData) {
317
+ return extractPayload(protocolData, K900LengthCodec.Endian.BE);
318
+ }
319
+
320
+ /**
321
+ * Extract payload from a K900 STRING frame, reading the length field with an explicit
322
+ * endianness. Prefer {@link #extractPayloadAuto} on receive when the peer endianness is not yet
323
+ * negotiated.
324
+ */
325
+ public static byte[] extractPayload(byte[] protocolData, K900LengthCodec.Endian endian) {
307
326
  if (!isK900ProtocolFormat(protocolData) || protocolData.length < 7) {
308
327
  return null;
309
328
  }
310
329
 
311
- // Extract length (big-endian)
312
- int length = ((protocolData[3] & 0xFF) << 8) | (protocolData[4] & 0xFF);
313
-
314
- // Original little-endian implementation (commented out)
315
- // int length = (protocolData[3] & 0xFF) | ((protocolData[4] & 0xFF) << 8);
330
+ int length = K900LengthCodec.readLength(protocolData, 3, endian);
316
331
 
317
332
  if (length + 7 > protocolData.length) {
318
333
  return null; // Invalid length
319
334
  }
320
335
 
321
- // Extract payload
322
336
  byte[] payload = new byte[length];
323
337
  System.arraycopy(protocolData, 5, payload, 0, length);
324
338
  return payload;
325
339
  }
326
340
 
327
341
  /**
328
- * Extract payload from K900 protocol formatted data received from device
329
- * Uses little-endian byte order for length field
330
- * @return Raw payload data or null if format is invalid
342
+ * Extract payload from a K900 STRING frame, heuristically detecting the length field's
343
+ * endianness. Safe RX entry point when the peer endianness is not yet negotiated (fixes the
344
+ * "Extracted length=9472" misframe against legacy big-endian BES firmware).
331
345
  */
332
- public static byte[] extractPayloadFromK900(byte[] protocolData) {
346
+ public static byte[] extractPayloadAuto(byte[] protocolData) {
333
347
  if (!isK900ProtocolFormat(protocolData) || protocolData.length < 7) {
334
- Log.e("K900ProtocolUtils", "extractPayloadFromK900: Not K900 format or too short. Length=" +
335
- (protocolData != null ? protocolData.length : 0));
336
348
  return null;
337
349
  }
338
-
339
- // Extract length (little-endian for device-to-phone)
340
- int length = (protocolData[3] & 0xFF) | ((protocolData[4] & 0xFF) << 8);
341
-
342
- Log.d("K900ProtocolUtils", "extractPayloadFromK900: Extracted length=" + length +
343
- ", message length=" + protocolData.length + ", expected total=" + (length + 7));
344
-
345
- if (length + 7 > protocolData.length) {
346
- Log.e("K900ProtocolUtils", "extractPayloadFromK900: Invalid length. Need " +
347
- (length + 7) + " bytes but have " + protocolData.length);
348
- return null; // Invalid length
350
+ K900LengthCodec.Detected detected = K900LengthCodec.detectLength(protocolData);
351
+ if (detected == null) {
352
+ return null;
349
353
  }
350
-
351
- // Extract payload
352
- byte[] payload = new byte[length];
353
- System.arraycopy(protocolData, 5, payload, 0, length);
354
+ byte[] payload = new byte[detected.length];
355
+ System.arraycopy(protocolData, 5, payload, 0, detected.length);
354
356
  return payload;
355
357
  }
356
358
 
359
+ /**
360
+ * Extract payload from K900 protocol formatted data received from device
361
+ * Uses little-endian byte order for length field
362
+ * @return Raw payload data or null if format is invalid
363
+ */
364
+ public static byte[] extractPayloadFromK900(byte[] protocolData) {
365
+ // Retained for backward compatibility: device-to-phone frames may arrive in either
366
+ // endianness depending on firmware vintage, so auto-detect rather than assuming LE.
367
+ return extractPayloadAuto(protocolData);
368
+ }
369
+
357
370
  /**
358
371
  * Process received bytes from Bluetooth into a JSON object
359
372
  * Handles K900 protocol format detection, payload extraction, and C-field unwrapping
@@ -379,8 +392,13 @@ public class K900ProtocolUtils {
379
392
  // Extract the command type
380
393
  byte commandType = data[2];
381
394
 
382
- // Extract the length using big-endian format (MSB first)
383
- int payloadLength = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF);
395
+ // Extract the length, auto-detecting endianness so we parse both legacy big-endian and
396
+ // wire-v2 little-endian frames.
397
+ K900LengthCodec.Detected detected = K900LengthCodec.detectLength(data);
398
+ int payloadLength =
399
+ detected != null
400
+ ? detected.length
401
+ : ((data[3] & 0xFF) | ((data[4] & 0xFF) << 8));
384
402
 
385
403
  android.util.Log.d("K900ProtocolUtils", "Command type: 0x" + String.format("%02X", commandType) +
386
404
  ", Payload length: " + payloadLength);
@@ -565,6 +583,23 @@ public class K900ProtocolUtils {
565
583
  return false;
566
584
  }
567
585
 
586
+ public static boolean isBinaryFrame(byte[] data) {
587
+ return BleWireProtocol.isBinaryFrame(data);
588
+ }
589
+
590
+ public static byte[] packBinaryFragment(
591
+ byte flags, int msgId, int fragIdx, int fragCount, byte[] payload) {
592
+ return BleWireProtocol.packBinaryFragment(flags, msgId, fragIdx, fragCount, payload);
593
+ }
594
+
595
+ public static BleWireProtocol.BinaryFragmentInfo extractBinaryFragmentInfo(byte[] frame) {
596
+ return BleWireProtocol.extractBinaryFragmentInfo(frame);
597
+ }
598
+
599
+ public static byte[] extractBinaryPayload(byte[] frame) {
600
+ return BleWireProtocol.extractBinaryPayload(frame);
601
+ }
602
+
568
603
  /**
569
604
  * Inner class to represent file packet information
570
605
  */
@@ -15,6 +15,48 @@ public class MessageChunkReassembler {
15
15
 
16
16
  private final Map<String, ChunkSession> activeSessions = new ConcurrentHashMap<>();
17
17
 
18
+ private final Map<Integer, BinarySession> activeBinarySessions = new ConcurrentHashMap<>();
19
+
20
+ public byte[] addBinaryFragment(int msgId, int fragIdx, int fragCount, byte[] data) {
21
+ cleanupTimedOutSessions();
22
+
23
+ if (msgId < 0 || fragCount <= 0 || fragIdx < 0 || fragIdx >= fragCount || data == null) {
24
+ Log.w(TAG, "Dropping invalid binary fragment: msgId=" + msgId + ", index=" + fragIdx
25
+ + ", total=" + fragCount);
26
+ return null;
27
+ }
28
+
29
+ if (activeBinarySessions.size() >= MAX_CONCURRENT_SESSIONS
30
+ && !activeBinarySessions.containsKey(msgId)) {
31
+ removeOldestBinarySession();
32
+ }
33
+
34
+ BinarySession session = activeBinarySessions.compute(msgId, (ignored, existingSession) -> {
35
+ if (existingSession == null) {
36
+ return new BinarySession(msgId, fragCount);
37
+ }
38
+ if (existingSession.fragCount != fragCount) {
39
+ Log.w(TAG, "fragCount mismatch for msgId " + msgId + ", resetting session");
40
+ return new BinarySession(msgId, fragCount);
41
+ }
42
+ return existingSession;
43
+ });
44
+
45
+ if (!session.addFragment(fragIdx, data)) {
46
+ return null;
47
+ }
48
+
49
+ if (!session.isComplete()) {
50
+ return null;
51
+ }
52
+
53
+ byte[] reassembled = session.reassemble();
54
+ activeBinarySessions.remove(msgId);
55
+ Log.d(TAG, "Reassembled binary message of " + reassembled.length + " bytes from "
56
+ + fragCount + " fragments");
57
+ return reassembled;
58
+ }
59
+
18
60
  public String addChunk(String chunkId, int chunkIndex, int totalChunks, String data) {
19
61
  cleanupTimedOutSessions();
20
62
 
@@ -61,11 +103,28 @@ public class MessageChunkReassembler {
61
103
 
62
104
  public void clear() {
63
105
  activeSessions.clear();
106
+ activeBinarySessions.clear();
64
107
  }
65
108
 
66
109
  private void cleanupTimedOutSessions() {
67
110
  long now = System.currentTimeMillis();
68
111
  activeSessions.entrySet().removeIf(entry -> now - entry.getValue().createdAt > CHUNK_TIMEOUT_MS);
112
+ activeBinarySessions.entrySet()
113
+ .removeIf(entry -> now - entry.getValue().createdAt > CHUNK_TIMEOUT_MS);
114
+ }
115
+
116
+ private void removeOldestBinarySession() {
117
+ Integer oldestId = null;
118
+ long oldestTime = Long.MAX_VALUE;
119
+ for (Map.Entry<Integer, BinarySession> entry : activeBinarySessions.entrySet()) {
120
+ if (entry.getValue().createdAt < oldestTime) {
121
+ oldestTime = entry.getValue().createdAt;
122
+ oldestId = entry.getKey();
123
+ }
124
+ }
125
+ if (oldestId != null) {
126
+ activeBinarySessions.remove(oldestId);
127
+ }
69
128
  }
70
129
 
71
130
  private void removeOldestSession() {
@@ -119,4 +178,51 @@ public class MessageChunkReassembler {
119
178
  return reassembled.toString();
120
179
  }
121
180
  }
181
+
182
+ private static class BinarySession {
183
+ final int msgId;
184
+ final int fragCount;
185
+ final long createdAt;
186
+ final Map<Integer, byte[]> fragments;
187
+
188
+ BinarySession(int msgId, int fragCount) {
189
+ this.msgId = msgId;
190
+ this.fragCount = fragCount;
191
+ this.createdAt = System.currentTimeMillis();
192
+ this.fragments = new ConcurrentHashMap<>();
193
+ }
194
+
195
+ boolean addFragment(int index, byte[] data) {
196
+ if (index < 0 || index >= fragCount) {
197
+ return false;
198
+ }
199
+ fragments.put(index, data);
200
+ return true;
201
+ }
202
+
203
+ boolean isComplete() {
204
+ return fragments.size() == fragCount;
205
+ }
206
+
207
+ byte[] reassemble() {
208
+ int totalLen = 0;
209
+ for (int i = 0; i < fragCount; i++) {
210
+ byte[] fragment = fragments.get(i);
211
+ if (fragment == null) {
212
+ throw new IllegalStateException("Missing binary fragment " + i + " for msgId "
213
+ + msgId);
214
+ }
215
+ totalLen += fragment.length;
216
+ }
217
+
218
+ byte[] result = new byte[totalLen];
219
+ int offset = 0;
220
+ for (int i = 0; i < fragCount; i++) {
221
+ byte[] fragment = fragments.get(i);
222
+ System.arraycopy(fragment, 0, result, offset, fragment.length);
223
+ offset += fragment.length;
224
+ }
225
+ return result;
226
+ }
227
+ }
122
228
  }