@mentra/bluetooth-sdk 0.1.21-beta.0 → 0.1.21-beta.2
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 +22 -11
- package/android/build.gradle +3 -0
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +34 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +7 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +82 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +32 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +53 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Ar99.java +2653 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +161 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +41 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +45 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/StreamStatusEventTest.kt +29 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
- package/build/BluetoothSdk.types.d.ts +58 -25
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +2 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +1 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +8 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +6 -3
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/cameraRequestPayload.d.ts +1 -1
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +6 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +10 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +20 -0
- package/build/_private/sdkOtaManifest.d.ts.map +1 -0
- package/build/_private/sdkOtaManifest.js +28 -0
- package/build/_private/sdkOtaManifest.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +7 -1
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +41 -2
- package/ios/Source/BluetoothSdkDefaults.swift +2 -1
- package/ios/Source/Bridge.swift +15 -3
- package/ios/Source/DeviceManager.swift +67 -17
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +37 -8
- package/ios/Source/OtaManifest.swift +47 -4
- package/ios/Source/camera/CameraModels.swift +20 -5
- package/ios/Source/internal/BleTraceLogger.swift +1 -1
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +39 -0
- package/ios/Source/sgcs/Ar99.swift +2451 -0
- package/ios/Source/sgcs/MentraLive.swift +174 -38
- package/ios/Source/sgcs/SGCManager.swift +8 -0
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/streaming/StreamModels.swift +71 -7
- package/ios/Source/types/DeviceModels.swift +13 -0
- package/ios/Source/utils/BleJsonCompact.swift +8 -0
- package/ios/Source/utils/Constants.swift +2 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +4 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +61 -28
- package/src/_internal.ts +1 -0
- package/src/_private/BluetoothSdkModule.ts +20 -6
- package/src/_private/cameraRequestPayload.ts +8 -2
- package/src/_private/photoRequestPayload.ts +12 -2
- package/src/_private/sdkOtaManifest.ts +31 -0
- package/src/index.ts +9 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.utils.audio;
|
|
2
|
+
|
|
3
|
+
import android.util.Log;
|
|
4
|
+
|
|
5
|
+
import java.util.Arrays;
|
|
6
|
+
import java.util.concurrent.ArrayBlockingQueue;
|
|
7
|
+
|
|
8
|
+
import io.github.jaredmdobson.OpusDecoder;
|
|
9
|
+
import io.github.jaredmdobson.OpusException;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* AR99 Opus decoder worker.
|
|
13
|
+
*
|
|
14
|
+
* <p>The caller feeds BLE notify bytes through {@link #submitBleNotify(byte[])}. This class owns
|
|
15
|
+
* queueing, frame reassembly, Opus decoder state, and decoded PCM callbacks.
|
|
16
|
+
*/
|
|
17
|
+
public final class Ar99OpusPcmDecoder extends Thread {
|
|
18
|
+
public interface Callback {
|
|
19
|
+
void onPcmDecoded(byte[] pcmData);
|
|
20
|
+
|
|
21
|
+
default void onNotifyProcessed(int rawLen, int pcmFrames, int pcmBytesOut) {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private enum TaskType {
|
|
25
|
+
DECODE,
|
|
26
|
+
RESET,
|
|
27
|
+
STOP
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private static final class Task {
|
|
31
|
+
final TaskType type;
|
|
32
|
+
final byte[] payload;
|
|
33
|
+
|
|
34
|
+
Task(TaskType type, byte[] payload) {
|
|
35
|
+
this.type = type;
|
|
36
|
+
this.payload = payload;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private static final int SAMPLE_RATE = 16000;
|
|
41
|
+
private static final int CHANNELS = 1;
|
|
42
|
+
private static final int MAX_FRAME_SAMPLES = 5760;
|
|
43
|
+
private static final int FRAME_BYTES = 48;
|
|
44
|
+
private static final int HEADER_BYTES = 8;
|
|
45
|
+
private static final int MAX_BUFFER_SIZE = 10000;
|
|
46
|
+
private static final int TASK_QUEUE_CAPACITY = 256;
|
|
47
|
+
private static final int MAX_DIAG_FRAMES = 8;
|
|
48
|
+
private static final byte[] EXPECTED_PREFIX = new byte[] {0x00, 0x00, 0x00, 0x28};
|
|
49
|
+
private static final String TAG = "Ar99OpusPcmDecoder";
|
|
50
|
+
|
|
51
|
+
private final Callback callback;
|
|
52
|
+
private final OpusDecoder decoder;
|
|
53
|
+
private final ArrayBlockingQueue<Task> taskQueue = new ArrayBlockingQueue<>(TASK_QUEUE_CAPACITY);
|
|
54
|
+
|
|
55
|
+
private volatile boolean running = true;
|
|
56
|
+
|
|
57
|
+
private byte[] buffer = new byte[0];
|
|
58
|
+
private long totalNotifyCount = 0L;
|
|
59
|
+
private long totalFrameCount = 0L;
|
|
60
|
+
private long totalDecodeFailCount = 0L;
|
|
61
|
+
private long totalHeaderMismatchCount = 0L;
|
|
62
|
+
private long totalShortChunkCount = 0L;
|
|
63
|
+
private long totalResyncDropBytes = 0L;
|
|
64
|
+
private boolean headerLooksStable = true;
|
|
65
|
+
|
|
66
|
+
public Ar99OpusPcmDecoder(Callback callback) throws OpusException {
|
|
67
|
+
super("ar99-opus-decode");
|
|
68
|
+
this.callback = callback;
|
|
69
|
+
this.decoder = new OpusDecoder(SAMPLE_RATE, CHANNELS);
|
|
70
|
+
start();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public void submitBleNotify(byte[] opusPayload) {
|
|
74
|
+
if (!running || opusPayload == null || opusPayload.length == 0) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
byte[] payloadCopy = Arrays.copyOf(opusPayload, opusPayload.length);
|
|
78
|
+
if (!taskQueue.offer(new Task(TaskType.DECODE, payloadCopy))) {
|
|
79
|
+
Log.w(TAG, "AR99 Opus task queue full, drop notify len=" + payloadCopy.length);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public void resetSession() {
|
|
84
|
+
if (!running) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
taskQueue.clear();
|
|
88
|
+
taskQueue.offer(new Task(TaskType.RESET, null));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public void resetBuffer() {
|
|
92
|
+
resetSession();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public void shutdownDecoder() {
|
|
96
|
+
if (!running) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
running = false;
|
|
100
|
+
taskQueue.clear();
|
|
101
|
+
taskQueue.offer(new Task(TaskType.STOP, null));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public synchronized byte[] decodePacketToPcm16Mono(byte[] opusPayload) {
|
|
105
|
+
return decodePayload(opusPayload);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Override
|
|
109
|
+
public void run() {
|
|
110
|
+
try {
|
|
111
|
+
while (true) {
|
|
112
|
+
Task task = taskQueue.take();
|
|
113
|
+
if (task.type == TaskType.STOP) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (task.type == TaskType.RESET) {
|
|
117
|
+
resetDecoderState();
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (task.payload != null && task.payload.length > 0) {
|
|
121
|
+
handleChunk(task.payload);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (InterruptedException e) {
|
|
125
|
+
if (running) {
|
|
126
|
+
Log.w(TAG, "AR99 Opus worker interrupted: " + e.getMessage());
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
taskQueue.clear();
|
|
130
|
+
resetDecoderState();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private synchronized void resetDecoderState() {
|
|
135
|
+
decoder.resetState();
|
|
136
|
+
buffer = new byte[0];
|
|
137
|
+
totalNotifyCount = 0L;
|
|
138
|
+
totalFrameCount = 0L;
|
|
139
|
+
totalDecodeFailCount = 0L;
|
|
140
|
+
totalHeaderMismatchCount = 0L;
|
|
141
|
+
totalShortChunkCount = 0L;
|
|
142
|
+
totalResyncDropBytes = 0L;
|
|
143
|
+
headerLooksStable = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private synchronized void handleChunk(byte[] chunk) {
|
|
147
|
+
totalNotifyCount++;
|
|
148
|
+
if (chunk.length < FRAME_BYTES) {
|
|
149
|
+
totalShortChunkCount++;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (buffer.length + chunk.length > MAX_BUFFER_SIZE) {
|
|
153
|
+
Log.w(
|
|
154
|
+
TAG,
|
|
155
|
+
"AR99 Opus buffer overflow, clear"
|
|
156
|
+
+ ", oldBuf="
|
|
157
|
+
+ buffer.length
|
|
158
|
+
+ ", chunk="
|
|
159
|
+
+ chunk.length
|
|
160
|
+
+ ", notify#="
|
|
161
|
+
+ totalNotifyCount);
|
|
162
|
+
buffer = new byte[0];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
int oldBufferLen = buffer.length;
|
|
166
|
+
buffer = concat(buffer, chunk);
|
|
167
|
+
|
|
168
|
+
int pcmFrames = 0;
|
|
169
|
+
int pcmBytesOut = 0;
|
|
170
|
+
int decodeFail = 0;
|
|
171
|
+
int skipPayload = 0;
|
|
172
|
+
int headerMismatch = 0;
|
|
173
|
+
|
|
174
|
+
while (buffer.length >= EXPECTED_PREFIX.length) {
|
|
175
|
+
int frameStart = findFrameStart(buffer, 0);
|
|
176
|
+
if (frameStart < 0) {
|
|
177
|
+
int keep = Math.min(buffer.length, EXPECTED_PREFIX.length - 1);
|
|
178
|
+
int drop = buffer.length - keep;
|
|
179
|
+
if (drop > 0) {
|
|
180
|
+
totalResyncDropBytes += drop;
|
|
181
|
+
headerLooksStable = false;
|
|
182
|
+
Log.w(
|
|
183
|
+
TAG,
|
|
184
|
+
"AR99 Opus resync drop(no header)"
|
|
185
|
+
+ " notify#="
|
|
186
|
+
+ totalNotifyCount
|
|
187
|
+
+ " dropBytes="
|
|
188
|
+
+ drop
|
|
189
|
+
+ " keepTail="
|
|
190
|
+
+ keep
|
|
191
|
+
+ " tailHex="
|
|
192
|
+
+ hex(Arrays.copyOfRange(buffer, drop, buffer.length), keep));
|
|
193
|
+
buffer = Arrays.copyOfRange(buffer, drop, buffer.length);
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (frameStart > 0) {
|
|
199
|
+
totalResyncDropBytes += frameStart;
|
|
200
|
+
headerLooksStable = false;
|
|
201
|
+
headerMismatch++;
|
|
202
|
+
totalHeaderMismatchCount++;
|
|
203
|
+
Log.w(
|
|
204
|
+
TAG,
|
|
205
|
+
"AR99 Opus resync shift"
|
|
206
|
+
+ " notify#="
|
|
207
|
+
+ totalNotifyCount
|
|
208
|
+
+ " dropBytes="
|
|
209
|
+
+ frameStart
|
|
210
|
+
+ " bufLen="
|
|
211
|
+
+ buffer.length
|
|
212
|
+
+ " droppedHex="
|
|
213
|
+
+ hex(Arrays.copyOfRange(buffer, 0, frameStart), Math.min(frameStart, 24)));
|
|
214
|
+
buffer = Arrays.copyOfRange(buffer, frameStart, buffer.length);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (buffer.length < FRAME_BYTES) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
byte[] frame = Arrays.copyOfRange(buffer, 0, FRAME_BYTES);
|
|
222
|
+
buffer = Arrays.copyOfRange(buffer, FRAME_BYTES, buffer.length);
|
|
223
|
+
totalFrameCount++;
|
|
224
|
+
|
|
225
|
+
boolean prefixOk = true;
|
|
226
|
+
for (int i = 0; i < EXPECTED_PREFIX.length; i++) {
|
|
227
|
+
if (frame[i] != EXPECTED_PREFIX[i]) {
|
|
228
|
+
prefixOk = false;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!prefixOk) {
|
|
233
|
+
headerMismatch++;
|
|
234
|
+
totalHeaderMismatchCount++;
|
|
235
|
+
headerLooksStable = false;
|
|
236
|
+
if (totalFrameCount <= MAX_DIAG_FRAMES || !headerLooksStable) {
|
|
237
|
+
Log.w(
|
|
238
|
+
TAG,
|
|
239
|
+
"AR99 Opus frame header mismatch"
|
|
240
|
+
+ " frame#="
|
|
241
|
+
+ totalFrameCount
|
|
242
|
+
+ " notify#="
|
|
243
|
+
+ totalNotifyCount
|
|
244
|
+
+ " remain="
|
|
245
|
+
+ buffer.length
|
|
246
|
+
+ " frameHex="
|
|
247
|
+
+ hex(frame, FRAME_BYTES));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
byte[] opusPayload = Arrays.copyOfRange(frame, HEADER_BYTES, FRAME_BYTES);
|
|
252
|
+
if (opusPayload.length == 0) {
|
|
253
|
+
skipPayload++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
byte[] pcm = decodePayload(opusPayload);
|
|
258
|
+
if (pcm != null && pcm.length > 0) {
|
|
259
|
+
pcmFrames++;
|
|
260
|
+
pcmBytesOut += pcm.length;
|
|
261
|
+
if (callback != null) {
|
|
262
|
+
callback.onPcmDecoded(pcm);
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
decodeFail++;
|
|
266
|
+
totalDecodeFailCount++;
|
|
267
|
+
if (totalFrameCount <= MAX_DIAG_FRAMES || !headerLooksStable) {
|
|
268
|
+
Log.w(
|
|
269
|
+
TAG,
|
|
270
|
+
"AR99 Opus frame decode failed"
|
|
271
|
+
+ " frame#="
|
|
272
|
+
+ totalFrameCount
|
|
273
|
+
+ " notify#="
|
|
274
|
+
+ totalNotifyCount
|
|
275
|
+
+ " prefixOk="
|
|
276
|
+
+ prefixOk
|
|
277
|
+
+ " frameHex="
|
|
278
|
+
+ hex(frame, FRAME_BYTES)
|
|
279
|
+
+ " payloadHex="
|
|
280
|
+
+ hex(opusPayload, opusPayload.length));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (chunk.length != FRAME_BYTES
|
|
286
|
+
|| oldBufferLen != 0
|
|
287
|
+
|| decodeFail > 0
|
|
288
|
+
|| headerMismatch > 0
|
|
289
|
+
|| skipPayload > 0
|
|
290
|
+
|| pcmFrames == 0
|
|
291
|
+
|| !headerLooksStable) {
|
|
292
|
+
Log.d(
|
|
293
|
+
TAG,
|
|
294
|
+
"AR99 Opus decode summary"
|
|
295
|
+
+ " notify#="
|
|
296
|
+
+ totalNotifyCount
|
|
297
|
+
+ " inLen="
|
|
298
|
+
+ chunk.length
|
|
299
|
+
+ " pcmFrames="
|
|
300
|
+
+ pcmFrames
|
|
301
|
+
+ " decodeFail="
|
|
302
|
+
+ decodeFail
|
|
303
|
+
+ " headerMismatch="
|
|
304
|
+
+ headerMismatch
|
|
305
|
+
+ " skipPayload="
|
|
306
|
+
+ skipPayload
|
|
307
|
+
+ " bufRemain="
|
|
308
|
+
+ buffer.length
|
|
309
|
+
+ " totals={frames="
|
|
310
|
+
+ totalFrameCount
|
|
311
|
+
+ ",fail="
|
|
312
|
+
+ totalDecodeFailCount
|
|
313
|
+
+ ",headerMismatch="
|
|
314
|
+
+ totalHeaderMismatchCount
|
|
315
|
+
+ ",shortChunk="
|
|
316
|
+
+ totalShortChunkCount
|
|
317
|
+
+ ",resyncDropBytes="
|
|
318
|
+
+ totalResyncDropBytes
|
|
319
|
+
+ "}");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (callback != null) {
|
|
323
|
+
callback.onNotifyProcessed(chunk.length, pcmFrames, pcmBytesOut);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private byte[] decodePayload(byte[] opusPayload) {
|
|
328
|
+
if (opusPayload == null || opusPayload.length == 0) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
byte[] pcm = new byte[MAX_FRAME_SAMPLES * CHANNELS * 2];
|
|
332
|
+
try {
|
|
333
|
+
int samples = decoder.decode(opusPayload, 0, opusPayload.length, pcm, 0, MAX_FRAME_SAMPLES, false);
|
|
334
|
+
if (samples <= 0) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
int bytes = samples * CHANNELS * 2;
|
|
338
|
+
return Arrays.copyOf(pcm, bytes);
|
|
339
|
+
} catch (OpusException | AssertionError | RuntimeException e) {
|
|
340
|
+
Log.w(
|
|
341
|
+
TAG,
|
|
342
|
+
"AR99 Opus decode failed len="
|
|
343
|
+
+ opusPayload.length
|
|
344
|
+
+ ", payloadHex="
|
|
345
|
+
+ hex(opusPayload, opusPayload.length)
|
|
346
|
+
+ ": "
|
|
347
|
+
+ e.getMessage());
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private static int findFrameStart(byte[] data, int fromIndex) {
|
|
353
|
+
if (data == null || data.length < EXPECTED_PREFIX.length) {
|
|
354
|
+
return -1;
|
|
355
|
+
}
|
|
356
|
+
int max = data.length - EXPECTED_PREFIX.length;
|
|
357
|
+
for (int i = Math.max(0, fromIndex); i <= max; i++) {
|
|
358
|
+
boolean match = true;
|
|
359
|
+
for (int j = 0; j < EXPECTED_PREFIX.length; j++) {
|
|
360
|
+
if (data[i + j] != EXPECTED_PREFIX[j]) {
|
|
361
|
+
match = false;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (match) {
|
|
366
|
+
return i;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return -1;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private static String hex(byte[] data, int maxBytes) {
|
|
373
|
+
if (data == null || data.length == 0 || maxBytes <= 0) {
|
|
374
|
+
return "";
|
|
375
|
+
}
|
|
376
|
+
int len = Math.min(data.length, maxBytes);
|
|
377
|
+
StringBuilder sb = new StringBuilder(len * 3);
|
|
378
|
+
for (int i = 0; i < len; i++) {
|
|
379
|
+
if (i > 0) {
|
|
380
|
+
sb.append(' ');
|
|
381
|
+
}
|
|
382
|
+
sb.append(String.format("%02X", data[i] & 0xFF));
|
|
383
|
+
}
|
|
384
|
+
if (data.length > len) {
|
|
385
|
+
sb.append("...");
|
|
386
|
+
}
|
|
387
|
+
return sb.toString();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private static byte[] concat(byte[] a, byte[] b) {
|
|
391
|
+
if (a.length == 0) {
|
|
392
|
+
return Arrays.copyOf(b, b.length);
|
|
393
|
+
}
|
|
394
|
+
byte[] out = Arrays.copyOf(a, a.length + b.length);
|
|
395
|
+
System.arraycopy(b, 0, out, a.length, b.length);
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertFalse
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Assert.fail
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
import org.junit.runner.RunWith
|
|
10
|
+
import org.robolectric.RobolectricTestRunner
|
|
11
|
+
import org.robolectric.annotation.Config
|
|
12
|
+
|
|
13
|
+
@RunWith(RobolectricTestRunner::class)
|
|
14
|
+
@Config(sdk = [33])
|
|
15
|
+
class OtaManifestDowngradeTest {
|
|
16
|
+
|
|
17
|
+
private fun manifest(versionCode: Long): JSONObject =
|
|
18
|
+
JSONObject()
|
|
19
|
+
.put("apps", JSONObject().put("com.mentra.asg_client", JSONObject().put("versionCode", versionCode)))
|
|
20
|
+
|
|
21
|
+
private fun hasUpdate(
|
|
22
|
+
currentBuildNumber: String,
|
|
23
|
+
manifest: JSONObject,
|
|
24
|
+
floor: Long = 0L,
|
|
25
|
+
): Boolean =
|
|
26
|
+
OtaManifestChecker.hasUpdate(
|
|
27
|
+
currentBuildNumber = currentBuildNumber,
|
|
28
|
+
currentMtkVersion = "",
|
|
29
|
+
currentBesVersion = "",
|
|
30
|
+
manifest = manifest,
|
|
31
|
+
downgradeFloorVersionCode = floor,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@Test
|
|
35
|
+
fun upgradeIsAlwaysAnUpdate() {
|
|
36
|
+
assertTrue(hasUpdate("49000000", manifest(49076573L)))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
fun exactPinIsNotAnUpdateEvenWithAnEnabledFloor() {
|
|
41
|
+
assertFalse(hasUpdate("49000000", manifest(49000000L), floor = 49000000L))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun downgradeNeedsAnEnabledFloor() {
|
|
46
|
+
// Default floor 0 (fail closed) disables downgrades for native/OEM callers.
|
|
47
|
+
assertFalse(hasUpdate("49076573", manifest(49000000L)))
|
|
48
|
+
// A positive floor at/below the pin enables it.
|
|
49
|
+
assertTrue(hasUpdate("49076573", manifest(49000000L), floor = 49000000L))
|
|
50
|
+
// A pin below the floor is refused.
|
|
51
|
+
assertFalse(hasUpdate("49076573", manifest(48999999L), floor = 49000000L))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test
|
|
55
|
+
fun exactPinIsNotAnUpdate() {
|
|
56
|
+
assertFalse(hasUpdate("49076573", manifest(49076573L)))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private fun legacyTopLevelManifest(versionCode: Long): JSONObject =
|
|
60
|
+
JSONObject().put("versionCode", versionCode)
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
fun legacyTopLevelManifestStaysUpgradeOnly() {
|
|
64
|
+
// No apps entry -> not an exact pin -> upgrade-only, never a false downgrade.
|
|
65
|
+
assertFalse(hasUpdate("49076573", legacyTopLevelManifest(49000000L)))
|
|
66
|
+
assertTrue(hasUpdate("49000000", legacyTopLevelManifest(49076573L)))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Test
|
|
70
|
+
fun zeroedPinOnModernGlassesIsAnError() {
|
|
71
|
+
// An unverifiable pin must never present as "no update" on modern glasses.
|
|
72
|
+
try {
|
|
73
|
+
hasUpdate("49076573", manifest(0L))
|
|
74
|
+
fail("expected invalid_ota_manifest")
|
|
75
|
+
} catch (e: BluetoothSdkException) {
|
|
76
|
+
assertEquals("invalid_ota_manifest", e.code)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Test
|
|
81
|
+
fun zeroedRescuePinIsInertForLegacyGlasses() {
|
|
82
|
+
// Pre-39 glasses are legitimately checked against the frozen rescue manifests, whose
|
|
83
|
+
// zeroed ASG pin must read as no APK update (the MTK patch chain is what matters there).
|
|
84
|
+
assertFalse(hasUpdate("38", manifest(0L)))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class StreamStatusEventTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun `stream telemetry survives native status normalization`() {
|
|
9
|
+
val event =
|
|
10
|
+
StreamStatusEvent(
|
|
11
|
+
mapOf(
|
|
12
|
+
"status" to "streaming",
|
|
13
|
+
"streamId" to "stream-1",
|
|
14
|
+
"stats" to
|
|
15
|
+
mapOf(
|
|
16
|
+
"bitrate" to 912_345L,
|
|
17
|
+
"fps" to 19.8,
|
|
18
|
+
"droppedFrames" to 2L,
|
|
19
|
+
"duration" to 31L,
|
|
20
|
+
"temperatureC" to 54.6,
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
assertThat(event.stats?.bitrate).isEqualTo(912_345L)
|
|
26
|
+
assertThat(event.stats?.temperatureC).isEqualTo(54.6)
|
|
27
|
+
assertThat((event.values["stats"] as Map<*, *>)["duration"]).isEqualTo(31L)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -5,6 +5,7 @@ import com.mentra.bluetoothsdk.PhotoMode
|
|
|
5
5
|
import com.mentra.bluetoothsdk.PhotoRequest
|
|
6
6
|
import com.mentra.bluetoothsdk.PhotoSize
|
|
7
7
|
import org.assertj.core.api.Assertions.assertThat
|
|
8
|
+
import org.assertj.core.api.Assertions.assertThatThrownBy
|
|
8
9
|
import org.junit.Test
|
|
9
10
|
|
|
10
11
|
class PhotoRequestTest {
|
|
@@ -108,4 +109,33 @@ class PhotoRequestTest {
|
|
|
108
109
|
|
|
109
110
|
assertThat(request.transferMethod).isEqualTo("ble")
|
|
110
111
|
}
|
|
112
|
+
|
|
113
|
+
@Test
|
|
114
|
+
fun `fromMap preserves direct transfer without BLE fallback`() {
|
|
115
|
+
val request =
|
|
116
|
+
PhotoRequest.fromMap(
|
|
117
|
+
mapOf(
|
|
118
|
+
"size" to "medium",
|
|
119
|
+
"transferMethod" to "direct",
|
|
120
|
+
"webhookUrl" to "https://example.com/upload",
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
assertThat(request.transferMethod).isEqualTo("direct")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
fun `fromMap rejects an unknown transfer method`() {
|
|
129
|
+
assertThatThrownBy {
|
|
130
|
+
PhotoRequest.fromMap(
|
|
131
|
+
mapOf(
|
|
132
|
+
"size" to "medium",
|
|
133
|
+
"transferMethod" to "wifi",
|
|
134
|
+
"webhookUrl" to "https://example.com/upload",
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
.isInstanceOf(IllegalArgumentException::class.java)
|
|
139
|
+
.hasMessage("Invalid transferMethod \"wifi\". Expected auto, direct, or ble.")
|
|
140
|
+
}
|
|
111
141
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs;
|
|
2
|
+
|
|
3
|
+
import static org.assertj.core.api.Assertions.assertThat;
|
|
4
|
+
|
|
5
|
+
import org.junit.Test;
|
|
6
|
+
|
|
7
|
+
public class Ar99AdvertisementMatcherTest {
|
|
8
|
+
|
|
9
|
+
@Test
|
|
10
|
+
public void supportsOnlyDocumentedProjectIdentifiers() {
|
|
11
|
+
assertThat(Ar99.isSupportedProjectName("AR99")).isTrue();
|
|
12
|
+
assertThat(Ar99.isSupportedProjectName("ar99")).isTrue();
|
|
13
|
+
|
|
14
|
+
assertThat(Ar99.isSupportedProjectName("AF98")).isFalse();
|
|
15
|
+
assertThat(Ar99.isSupportedProjectName("AF99")).isFalse();
|
|
16
|
+
assertThat(Ar99.isSupportedProjectName("HVXM")).isFalse();
|
|
17
|
+
assertThat(Ar99.isSupportedProjectName("HVXF")).isFalse();
|
|
18
|
+
assertThat(Ar99.isSupportedProjectName("AR99X")).isFalse();
|
|
19
|
+
assertThat(Ar99.isSupportedProjectName("")).isFalse();
|
|
20
|
+
assertThat(Ar99.isSupportedProjectName(null)).isFalse();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -135,6 +135,26 @@ public class BleJsonCompactTest {
|
|
|
135
135
|
assertEquals("wifi_scan_result", BleJsonCompact.encode(wifiScan).getString("t"));
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
@Test
|
|
139
|
+
public void streamTelemetryRoundTripsWithCompactKeys() throws Exception {
|
|
140
|
+
JSONObject status =
|
|
141
|
+
new JSONObject(
|
|
142
|
+
"{\"type\":\"stream_status\",\"status\":\"streaming\","
|
|
143
|
+
+ "\"stats\":{\"bitrate\":912345,\"fps\":19.8,"
|
|
144
|
+
+ "\"droppedFrames\":2,\"duration\":31,"
|
|
145
|
+
+ "\"temperatureC\":54.6}}\n");
|
|
146
|
+
|
|
147
|
+
JSONObject wire = BleJsonCompact.encode(status);
|
|
148
|
+
assertEquals(912345L, wire.getJSONObject("st").getLong("br"));
|
|
149
|
+
assertEquals(54.6d, wire.getJSONObject("st").getDouble("tc"), 0.001d);
|
|
150
|
+
|
|
151
|
+
JSONObject restored = BleJsonCompact.decode(wire);
|
|
152
|
+
JSONObject stats = restored.getJSONObject("stats");
|
|
153
|
+
assertEquals(19.8d, stats.getDouble("fps"), 0.001d);
|
|
154
|
+
assertEquals(2L, stats.getLong("droppedFrames"));
|
|
155
|
+
assertEquals(31L, stats.getLong("duration"));
|
|
156
|
+
}
|
|
157
|
+
|
|
138
158
|
@Test
|
|
139
159
|
public void decodeIfSupported_rejectsCompactLowRoi() throws Exception {
|
|
140
160
|
JSONObject compactPing = new JSONObject("{\"t\":\"ping\"}");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.utils
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class PhoneAudioMonitorTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun `keeps explicit playback hint while Android reports active media`() {
|
|
9
|
+
assertThat(
|
|
10
|
+
PhoneAudioMonitor.shouldKeepOwnAppAudioSignal(
|
|
11
|
+
signalledPlaying = true,
|
|
12
|
+
systemAudioPlaying = true,
|
|
13
|
+
nowMs = 10_000,
|
|
14
|
+
holdoffUntilMs = 1_000,
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
.isTrue()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Test
|
|
21
|
+
fun `keeps explicit playback hint during codec transition holdoff`() {
|
|
22
|
+
assertThat(
|
|
23
|
+
PhoneAudioMonitor.shouldKeepOwnAppAudioSignal(
|
|
24
|
+
signalledPlaying = true,
|
|
25
|
+
systemAudioPlaying = false,
|
|
26
|
+
nowMs = 500,
|
|
27
|
+
holdoffUntilMs = 1_000,
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
.isTrue()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test
|
|
34
|
+
fun `expires stale playback hint when Android media is inactive`() {
|
|
35
|
+
assertThat(
|
|
36
|
+
PhoneAudioMonitor.shouldKeepOwnAppAudioSignal(
|
|
37
|
+
signalledPlaying = true,
|
|
38
|
+
systemAudioPlaying = false,
|
|
39
|
+
nowMs = 1_000,
|
|
40
|
+
holdoffUntilMs = 1_000,
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
.isFalse()
|
|
44
|
+
}
|
|
45
|
+
}
|