@mentra/bluetooth-sdk 0.1.20 → 0.1.21-beta.1

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 (114) hide show
  1. package/README.md +27 -12
  2. package/android/build.gradle +3 -0
  3. package/android/src/main/AndroidManifest.xml +3 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +34 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +169 -26
  7. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +72 -16
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +127 -8
  9. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +4 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +400 -99
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
  13. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +18 -2
  14. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +1 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +53 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Ar99.java +2653 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +342 -42
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +7 -2
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
  29. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
  30. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
  31. package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
  32. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +52 -2
  33. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
  34. package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +7 -0
  35. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +8 -0
  36. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
  37. package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +45 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
  39. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
  40. package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
  41. package/android/src/test/java/com/mentra/bluetoothsdk/StreamStatusEventTest.kt +29 -0
  42. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +45 -0
  43. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
  44. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +20 -0
  45. package/android/src/test/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
  46. package/build/BluetoothSdk.types.d.ts +100 -22
  47. package/build/BluetoothSdk.types.d.ts.map +1 -1
  48. package/build/BluetoothSdk.types.js +2 -1
  49. package/build/BluetoothSdk.types.js.map +1 -1
  50. package/build/_internal.d.ts +3 -0
  51. package/build/_internal.d.ts.map +1 -1
  52. package/build/_internal.js +3 -0
  53. package/build/_internal.js.map +1 -1
  54. package/build/_private/BluetoothSdkModule.d.ts +31 -4
  55. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  56. package/build/_private/BluetoothSdkModule.js +26 -3
  57. package/build/_private/BluetoothSdkModule.js.map +1 -1
  58. package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
  59. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
  60. package/build/_private/MentraLocalNetworkModule.js +3 -0
  61. package/build/_private/MentraLocalNetworkModule.js.map +1 -0
  62. package/build/_private/cameraRequestPayload.d.ts +1 -1
  63. package/build/_private/cameraRequestPayload.d.ts.map +1 -1
  64. package/build/_private/cameraRequestPayload.js +7 -0
  65. package/build/_private/cameraRequestPayload.js.map +1 -1
  66. package/build/_private/photoRequestPayload.d.ts.map +1 -1
  67. package/build/_private/photoRequestPayload.js +10 -0
  68. package/build/_private/photoRequestPayload.js.map +1 -1
  69. package/build/_private/sdkOtaManifest.d.ts +20 -0
  70. package/build/_private/sdkOtaManifest.d.ts.map +1 -0
  71. package/build/_private/sdkOtaManifest.js +28 -0
  72. package/build/_private/sdkOtaManifest.js.map +1 -0
  73. package/build/index.d.ts +1 -1
  74. package/build/index.d.ts.map +1 -1
  75. package/build/index.js +11 -1
  76. package/build/index.js.map +1 -1
  77. package/expo-module.config.json +1 -0
  78. package/ios/BluetoothSdkModule.swift +113 -2
  79. package/ios/Source/BluetoothSdkDefaults.swift +2 -1
  80. package/ios/Source/Bridge.swift +87 -21
  81. package/ios/Source/DeviceManager.swift +118 -20
  82. package/ios/Source/DeviceStore.swift +4 -0
  83. package/ios/Source/MentraBluetoothSDK.swift +326 -33
  84. package/ios/Source/OtaManifest.swift +47 -4
  85. package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
  86. package/ios/Source/camera/CameraModels.swift +26 -6
  87. package/ios/Source/internal/BleTraceLogger.swift +1 -1
  88. package/ios/Source/internal/BluetoothSdkAnalytics.swift +39 -0
  89. package/ios/Source/sgcs/Ar99.swift +2451 -0
  90. package/ios/Source/sgcs/G1.swift +1 -1
  91. package/ios/Source/sgcs/G2.swift +19 -7
  92. package/ios/Source/sgcs/Mach1.swift +1 -1
  93. package/ios/Source/sgcs/MentraLive.swift +478 -61
  94. package/ios/Source/sgcs/MentraNex.swift +1 -1
  95. package/ios/Source/sgcs/Nimo.swift +1 -1
  96. package/ios/Source/sgcs/SGCManager.swift +13 -2
  97. package/ios/Source/sgcs/Simulated.swift +1 -1
  98. package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
  99. package/ios/Source/status/WifiHotspotStatus.swift +21 -2
  100. package/ios/Source/streaming/StreamModels.swift +68 -2
  101. package/ios/Source/tts/TTSTools.swift +1 -1
  102. package/ios/Source/types/DeviceModels.swift +13 -0
  103. package/ios/Source/utils/BleJsonCompact.swift +8 -0
  104. package/ios/Source/utils/Constants.swift +2 -0
  105. package/ios/Source/utils/PhoneAudioMonitor.swift +4 -0
  106. package/package.json +1 -2
  107. package/src/BluetoothSdk.types.ts +106 -24
  108. package/src/_internal.ts +3 -0
  109. package/src/_private/BluetoothSdkModule.ts +83 -18
  110. package/src/_private/MentraLocalNetworkModule.ts +50 -0
  111. package/src/_private/cameraRequestPayload.ts +9 -2
  112. package/src/_private/photoRequestPayload.ts +13 -4
  113. package/src/_private/sdkOtaManifest.ts +31 -0
  114. package/src/index.ts +13 -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
+ }