@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.
- package/README.md +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +52 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +119 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +22 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +457 -31
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +361 -67
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +7501 -3704
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +134 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleWireProtocol.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +4 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900LengthCodec.java +115 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +103 -68
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +93 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +111 -11
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +7 -9
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +17 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +158 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
- package/build/BluetoothSdk.types.d.ts +12 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +2 -0
- package/build/types/index.js.map +1 -0
- package/ios/LocalPhotoUploadServer.swift +78 -0
- package/ios/MentraPhotoReceiverModule.swift +10 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +63 -0
- package/ios/Source/DeviceManager.swift +134 -29
- package/ios/Source/DeviceStore.swift +0 -5
- package/ios/Source/MentraBluetoothSDK.swift +26 -23
- package/ios/Source/OtaManifest.swift +1 -1
- package/ios/Source/internal/BleTraceLogger.swift +192 -0
- package/ios/Source/sgcs/G2.swift +518 -11
- package/ios/Source/sgcs/MentraLive.swift +607 -39
- package/ios/Source/sgcs/MentraNex.swift +416 -32
- package/ios/Source/sgcs/SGCManager.swift +155 -0
- package/ios/Source/sgcs/mentraos_ble.pb.swift +726 -122
- package/ios/Source/status/DeviceStatus.swift +0 -8
- package/ios/Source/utils/BleJsonCompact.swift +395 -0
- package/ios/Source/utils/BleWireProtocol.swift +92 -0
- package/ios/Source/utils/MessageChunkReassembler.swift +82 -0
- package/ios/Source/utils/MessageChunker.swift +73 -0
- package/package.json +14 -7
- package/src/BluetoothSdk.types.ts +13 -1
- package/src/types/index.ts +7 -0
|
@@ -37,7 +37,10 @@ import org.json.JSONObject;
|
|
|
37
37
|
*/
|
|
38
38
|
public class BlePhotoUploadService {
|
|
39
39
|
private static final String TAG = "BlePhotoUploadService";
|
|
40
|
-
|
|
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
|
+
}
|
package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java
CHANGED
|
@@ -13,7 +13,7 @@ import okhttp3.RequestBody;
|
|
|
13
13
|
import okhttp3.Response;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* POSTs
|
|
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("
|
|
45
|
+
.addPathSegment("client")
|
|
46
|
+
.addPathSegment("reports")
|
|
46
47
|
.addPathSegment(incidentId)
|
|
47
|
-
.addPathSegment("
|
|
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,
|
|
105
|
-
result
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
|
186
|
+
// Then pack with BES2700 protocol format using the negotiated endianness
|
|
187
187
|
byte[] jsonBytes = wrappedJson.getBytes(StandardCharsets.UTF_8);
|
|
188
|
-
return
|
|
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 =
|
|
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
|
-
|
|
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
|
|
329
|
-
*
|
|
330
|
-
*
|
|
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[]
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
|
383
|
-
|
|
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
|
}
|