@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.
Files changed (77) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +135 -51
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +120 -17
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -6
  5. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -21
  6. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
  7. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +13 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +551 -159
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +361 -67
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +7501 -3704
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +134 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
  17. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -3
  18. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -5
  20. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleWireProtocol.java +108 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +4 -3
  22. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900LengthCodec.java +115 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +109 -73
  24. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +106 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +93 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +111 -11
  27. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
  28. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -9
  29. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +17 -1
  31. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +158 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
  36. package/build/BluetoothSdk.types.d.ts +37 -1
  37. package/build/BluetoothSdk.types.d.ts.map +1 -1
  38. package/build/BluetoothSdk.types.js.map +1 -1
  39. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  40. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  41. package/build/_private/BluetoothSdkModule.js.map +1 -1
  42. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  43. package/build/_private/photoRequestPayload.js +1 -0
  44. package/build/_private/photoRequestPayload.js.map +1 -1
  45. package/build/index.d.ts +1 -1
  46. package/build/index.d.ts.map +1 -1
  47. package/build/index.js +5 -0
  48. package/build/index.js.map +1 -1
  49. package/build/types/index.d.ts +3 -0
  50. package/build/types/index.d.ts.map +1 -0
  51. package/build/types/index.js +2 -0
  52. package/build/types/index.js.map +1 -0
  53. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  54. package/ios/Source/DeviceManager.swift +136 -30
  55. package/ios/Source/DeviceStore.swift +5 -5
  56. package/ios/Source/MentraBluetoothSDK.swift +40 -24
  57. package/ios/Source/OtaManifest.swift +1 -1
  58. package/ios/Source/camera/CameraModels.swift +17 -3
  59. package/ios/Source/sgcs/G1.swift +1 -1
  60. package/ios/Source/sgcs/G2.swift +518 -11
  61. package/ios/Source/sgcs/MentraLive.swift +443 -42
  62. package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
  63. package/ios/Source/sgcs/MentraNex.swift +416 -32
  64. package/ios/Source/sgcs/SGCManager.swift +155 -0
  65. package/ios/Source/sgcs/mentraos_ble.pb.swift +726 -122
  66. package/ios/Source/status/DeviceStatus.swift +0 -8
  67. package/ios/Source/utils/BleJsonCompact.swift +395 -0
  68. package/ios/Source/utils/BleWireProtocol.swift +92 -0
  69. package/ios/Source/utils/G1Text.swift +30 -1
  70. package/ios/Source/utils/MessageChunkReassembler.swift +82 -0
  71. package/ios/Source/utils/MessageChunker.swift +73 -0
  72. package/package.json +14 -7
  73. package/src/BluetoothSdk.types.ts +39 -1
  74. package/src/_private/BluetoothSdkModule.ts +4 -0
  75. package/src/_private/photoRequestPayload.ts +1 -0
  76. package/src/index.ts +6 -0
  77. package/src/types/index.ts +7 -0
@@ -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,9 +27,12 @@ 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
- public static final int FILE_PACK_SIZE = 400; // Max data size per packet
33
+ // Negotiated file protocol ceiling. Legacy/GATT transfers remain smaller; 800-byte frames are
34
+ // accepted only after file_payload_v2 negotiation and an open CoC channel.
35
+ public static final int FILE_PACK_SIZE = 800;
33
36
  public static final int LENGTH_FILE_START = 2;
34
37
  public static final int LENGTH_FILE_TYPE = 1;
35
38
  public static final int LENGTH_FILE_PACKSIZE = 2;
@@ -54,6 +57,10 @@ public class K900ProtocolUtils {
54
57
  * @return Byte array with packed data according to protocol format
55
58
  */
56
59
  public static byte[] packJsonCommand(String jsonData) {
60
+ return packJsonCommand(jsonData, K900LengthCodec.Endian.BE);
61
+ }
62
+
63
+ public static byte[] packJsonCommand(String jsonData, K900LengthCodec.Endian endian) {
57
64
  if (jsonData == null) {
58
65
  return null;
59
66
  }
@@ -68,7 +75,7 @@ public class K900ProtocolUtils {
68
75
 
69
76
  // Then pack with BES2700 protocol format
70
77
  byte[] jsonBytes = wrappedJson.getBytes(StandardCharsets.UTF_8);
71
- return packDataCommand(jsonBytes, CMD_TYPE_STRING);
78
+ return packDataCommand(jsonBytes, CMD_TYPE_STRING, endian);
72
79
 
73
80
  } catch (JSONException e) {
74
81
  android.util.Log.e("K900ProtocolUtils", "Error creating JSON wrapper", e);
@@ -85,6 +92,16 @@ public class K900ProtocolUtils {
85
92
  * @return Byte array with packed data according to protocol format
86
93
  */
87
94
  public static byte[] packDataCommand(byte[] data, byte cmdType) {
95
+ // Default to legacy big-endian until wire_caps negotiates little-endian.
96
+ return packDataCommand(data, cmdType, K900LengthCodec.Endian.BE);
97
+ }
98
+
99
+ /**
100
+ * Pack raw byte data with K900 BES2700 protocol format, writing the 2-byte length field with an
101
+ * explicit endianness. Use the negotiated per-link endianness so both legacy big-endian and
102
+ * wire-v2 little-endian peers can parse the frame.
103
+ */
104
+ public static byte[] packDataCommand(byte[] data, byte cmdType, K900LengthCodec.Endian endian) {
88
105
  if (data == null) {
89
106
  return null;
90
107
  }
@@ -101,13 +118,8 @@ public class K900ProtocolUtils {
101
118
  // Command type
102
119
  result[2] = cmdType;
103
120
 
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
121
+ // Length (2 bytes, negotiated endianness)
122
+ K900LengthCodec.writeLength(result, 3, dataLength, endian);
111
123
 
112
124
  // Copy the data
113
125
  System.arraycopy(data, 0, result, 5, dataLength);
@@ -129,34 +141,15 @@ public class K900ProtocolUtils {
129
141
  * @return Byte array with packed data according to protocol format
130
142
  */
131
143
  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]; // $
144
+ return packDataToK900(data, cmdType, K900LengthCodec.Endian.BE);
145
+ }
158
146
 
159
- return result;
147
+ /**
148
+ * Pack raw byte data with K900 BES2700 protocol format for phone-to-device communication.
149
+ * Format: ## + command_type + length(2bytes) + data + $$
150
+ */
151
+ public static byte[] packDataToK900(byte[] data, byte cmdType, K900LengthCodec.Endian endian) {
152
+ return packDataCommand(data, cmdType, endian);
160
153
  }
161
154
 
162
155
  /**
@@ -168,6 +161,15 @@ public class K900ProtocolUtils {
168
161
  * @return Byte array with packed data according to protocol format
169
162
  */
170
163
  public static byte[] packJsonToK900(String jsonData, boolean wakeup) {
164
+ return packJsonToK900(jsonData, wakeup, K900LengthCodec.Endian.BE);
165
+ }
166
+
167
+ /**
168
+ * Pack a C-wrapped JSON string for phone-to-glasses transmission, writing the STRING frame
169
+ * length with the negotiated per-link endianness. Legacy big-endian glasses require BE until
170
+ * they advertise {@code wire_caps.k900_le}.
171
+ */
172
+ public static byte[] packJsonToK900(String jsonData, boolean wakeup, K900LengthCodec.Endian endian) {
171
173
  if (jsonData == null) {
172
174
  return null;
173
175
  }
@@ -183,9 +185,9 @@ public class K900ProtocolUtils {
183
185
  // Convert to string
184
186
  String wrappedJson = wrapper.toString();
185
187
 
186
- // Then pack with BES2700 protocol format using little-endian
188
+ // Then pack with BES2700 protocol format using the negotiated endianness
187
189
  byte[] jsonBytes = wrappedJson.getBytes(StandardCharsets.UTF_8);
188
- return packDataToK900(jsonBytes, CMD_TYPE_STRING);
190
+ return packDataCommand(jsonBytes, CMD_TYPE_STRING, endian);
189
191
 
190
192
  } catch (JSONException e) {
191
193
  android.util.Log.e("K900ProtocolUtils", "Error creating JSON wrapper for K900", e);
@@ -203,6 +205,14 @@ public class K900ProtocolUtils {
203
205
  * @return Formatted bytes ready for transmission
204
206
  */
205
207
  public static byte[] formatMessageForTransmission(String jsonData) {
208
+ return formatMessageForTransmission(jsonData, K900LengthCodec.Endian.BE);
209
+ }
210
+
211
+ /**
212
+ * Format an ASG-client JSON message for transmission, writing the STRING frame length with the
213
+ * negotiated per-link endianness.
214
+ */
215
+ public static byte[] formatMessageForTransmission(String jsonData, K900LengthCodec.Endian endian) {
206
216
  try {
207
217
  android.util.Log.e("K900ProtocolUtils", "🔄 Formatting message: " + jsonData);
208
218
 
@@ -218,7 +228,9 @@ public class K900ProtocolUtils {
218
228
  android.util.Log.e("K900ProtocolUtils", "🔄 After C-wrapping: " + wrappedJson);
219
229
 
220
230
  // Now format with BES2700 protocol
221
- byte[] result = packDataCommand(wrappedJson.getBytes(StandardCharsets.UTF_8), CMD_TYPE_STRING);
231
+ byte[] result =
232
+ packDataCommand(
233
+ wrappedJson.getBytes(StandardCharsets.UTF_8), CMD_TYPE_STRING, endian);
222
234
 
223
235
  // Log some bytes for debugging
224
236
  StringBuilder hexDump = new StringBuilder();
@@ -233,7 +245,7 @@ public class K900ProtocolUtils {
233
245
  } catch (JSONException e) {
234
246
  android.util.Log.e("K900ProtocolUtils", "❌ Error in formatMessageForTransmission", e);
235
247
  // Fallback: if json is invalid, still try to pack it without validation
236
- return packJsonCommand(jsonData);
248
+ return packJsonCommand(jsonData, endian);
237
249
  }
238
250
  }
239
251
 
@@ -264,9 +276,6 @@ public class K900ProtocolUtils {
264
276
  return false;
265
277
  }
266
278
 
267
- Log.d("K900ProtocolUtils", "isK900ProtocolFormat: " + data[0] + " " + data[1]);
268
- Log.d("K900ProtocolUtils", "CMD_START_CODE: " + CMD_START_CODE[0] + " " + CMD_START_CODE[1]);
269
-
270
279
  return data[0] == CMD_START_CODE[0] &&
271
280
  data[1] == CMD_START_CODE[1];
272
281
  }
@@ -304,56 +313,59 @@ public class K900ProtocolUtils {
304
313
  * @return Raw payload data or null if format is invalid
305
314
  */
306
315
  public static byte[] extractPayload(byte[] protocolData) {
316
+ return extractPayload(protocolData, K900LengthCodec.Endian.BE);
317
+ }
318
+
319
+ /**
320
+ * Extract payload from a K900 STRING frame, reading the length field with an explicit
321
+ * endianness. Prefer {@link #extractPayloadAuto} on receive when the peer endianness is not yet
322
+ * negotiated.
323
+ */
324
+ public static byte[] extractPayload(byte[] protocolData, K900LengthCodec.Endian endian) {
307
325
  if (!isK900ProtocolFormat(protocolData) || protocolData.length < 7) {
308
326
  return null;
309
327
  }
310
328
 
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);
329
+ int length = K900LengthCodec.readLength(protocolData, 3, endian);
316
330
 
317
331
  if (length + 7 > protocolData.length) {
318
332
  return null; // Invalid length
319
333
  }
320
334
 
321
- // Extract payload
322
335
  byte[] payload = new byte[length];
323
336
  System.arraycopy(protocolData, 5, payload, 0, length);
324
337
  return payload;
325
338
  }
326
339
 
327
340
  /**
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
341
+ * Extract payload from a K900 STRING frame, heuristically detecting the length field's
342
+ * endianness. Safe RX entry point when the peer endianness is not yet negotiated (fixes the
343
+ * "Extracted length=9472" misframe against legacy big-endian BES firmware).
331
344
  */
332
- public static byte[] extractPayloadFromK900(byte[] protocolData) {
345
+ public static byte[] extractPayloadAuto(byte[] protocolData) {
333
346
  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
347
  return null;
337
348
  }
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
349
+ K900LengthCodec.Detected detected = K900LengthCodec.detectLength(protocolData);
350
+ if (detected == null) {
351
+ return null;
349
352
  }
350
-
351
- // Extract payload
352
- byte[] payload = new byte[length];
353
- System.arraycopy(protocolData, 5, payload, 0, length);
353
+ byte[] payload = new byte[detected.length];
354
+ System.arraycopy(protocolData, 5, payload, 0, detected.length);
354
355
  return payload;
355
356
  }
356
357
 
358
+ /**
359
+ * Extract payload from K900 protocol formatted data received from device
360
+ * Uses little-endian byte order for length field
361
+ * @return Raw payload data or null if format is invalid
362
+ */
363
+ public static byte[] extractPayloadFromK900(byte[] protocolData) {
364
+ // Retained for backward compatibility: device-to-phone frames may arrive in either
365
+ // endianness depending on firmware vintage, so auto-detect rather than assuming LE.
366
+ return extractPayloadAuto(protocolData);
367
+ }
368
+
357
369
  /**
358
370
  * Process received bytes from Bluetooth into a JSON object
359
371
  * Handles K900 protocol format detection, payload extraction, and C-field unwrapping
@@ -379,8 +391,13 @@ public class K900ProtocolUtils {
379
391
  // Extract the command type
380
392
  byte commandType = data[2];
381
393
 
382
- // Extract the length using big-endian format (MSB first)
383
- int payloadLength = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF);
394
+ // Extract the length, auto-detecting endianness so we parse both legacy big-endian and
395
+ // wire-v2 little-endian frames.
396
+ K900LengthCodec.Detected detected = K900LengthCodec.detectLength(data);
397
+ int payloadLength =
398
+ detected != null
399
+ ? detected.length
400
+ : ((data[3] & 0xFF) | ((data[4] & 0xFF) << 8));
384
401
 
385
402
  android.util.Log.d("K900ProtocolUtils", "Command type: 0x" + String.format("%02X", commandType) +
386
403
  ", Payload length: " + payloadLength);
@@ -565,6 +582,23 @@ public class K900ProtocolUtils {
565
582
  return false;
566
583
  }
567
584
 
585
+ public static boolean isBinaryFrame(byte[] data) {
586
+ return BleWireProtocol.isBinaryFrame(data);
587
+ }
588
+
589
+ public static byte[] packBinaryFragment(
590
+ byte flags, int msgId, int fragIdx, int fragCount, byte[] payload) {
591
+ return BleWireProtocol.packBinaryFragment(flags, msgId, fragIdx, fragCount, payload);
592
+ }
593
+
594
+ public static BleWireProtocol.BinaryFragmentInfo extractBinaryFragmentInfo(byte[] frame) {
595
+ return BleWireProtocol.extractBinaryFragmentInfo(frame);
596
+ }
597
+
598
+ public static byte[] extractBinaryPayload(byte[] frame) {
599
+ return BleWireProtocol.extractBinaryPayload(frame);
600
+ }
601
+
568
602
  /**
569
603
  * Inner class to represent file packet information
570
604
  */
@@ -757,7 +791,9 @@ public class K900ProtocolUtils {
757
791
  Log.e("K900ProtocolUtils", "File packet checksum failed. Expected: " +
758
792
  String.format("%02X", info.verifyCode) + ", Calculated: " +
759
793
  String.format("%02X", calculatedVerify));
760
- } else {
794
+ } else if (info.packIndex == 0
795
+ || info.packIndex % 32 == 0
796
+ || info.packIndex == (info.fileSize + FILE_PACK_SIZE - 1) / FILE_PACK_SIZE - 1) {
761
797
  Log.d("K900ProtocolUtils", "File packet extracted successfully: index=" + info.packIndex +
762
798
  ", size=" + info.packSize + ", fileName=" + info.fileName);
763
799
  }