@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
@@ -334,7 +334,7 @@ class MentraNex : SGCManager() {
334
334
  }
335
335
 
336
336
  // Network Management
337
- override fun requestWifiScan () { Bridge.log("Nex: requestWifiScan operation not supported") }
337
+ override fun requestWifiScan (scanId: String?) { Bridge.log("Nex: requestWifiScan operation not supported") }
338
338
  override fun sendWifiCredentials(ssid: String, password: String) { Bridge.log("Nex: sendWifiCredentials operation not supported") }
339
339
  override fun forgetWifiNetwork(ssid: String) { }
340
340
  override fun sendHotspotState(enabled: Boolean) { Bridge.log("Nex: sendHotspotState operation not supported") }
@@ -1427,7 +1427,7 @@ class Nimo : SGCManager() {
1427
1427
 
1428
1428
  // ---------- SGCManager: Network (no WiFi) ----------
1429
1429
 
1430
- override fun requestWifiScan() {}
1430
+ override fun requestWifiScan(scanId: String?) {}
1431
1431
  override fun sendWifiCredentials(ssid: String, password: String) {}
1432
1432
  override fun forgetWifiNetwork(ssid: String) {}
1433
1433
  override fun sendHotspotState(enabled: Boolean) {}
@@ -46,6 +46,8 @@ abstract class SGCManager {
46
46
 
47
47
  // Audio Control
48
48
  abstract fun setMicEnabled(enabled: Boolean)
49
+ open val isMicSuspendedForAudio: Boolean
50
+ get() = false
49
51
  abstract fun sortMicRanking(list: MutableList<String>): MutableList<String>
50
52
 
51
53
  // Camera & Media
@@ -296,12 +298,12 @@ abstract class SGCManager {
296
298
  abstract fun dbg2()
297
299
 
298
300
  // Network Management
299
- abstract fun requestWifiScan()
301
+ abstract fun requestWifiScan(scanId: String?)
300
302
  abstract fun sendWifiCredentials(ssid: String, password: String)
301
303
  abstract fun forgetWifiNetwork(ssid: String)
302
304
  abstract fun sendHotspotState(enabled: Boolean)
303
305
 
304
- /** Set glasses system clock (Mentra Live only; no-op on other devices). */
306
+ /** Set glasses system clock (Mentra Live and G2; no-op on other devices). */
305
307
  open fun sendSetSystemTime(timestampMs: Long) {
306
308
  Bridge.log("SGC: sendSetSystemTime not supported on $type")
307
309
  }
@@ -319,6 +321,9 @@ abstract class SGCManager {
319
321
  // Voice Activity Detection
320
322
  open fun sendVoiceActivityDetectionSetting() {}
321
323
 
324
+ // Mentra Live center-mic loudness / Barrier gate
325
+ open fun sendLoudnessGateSetting() {}
326
+
322
327
  // Start/stop LC3 audio playback from glasses based on the nex_lc3_audio_playback flag.
323
328
  open fun applyNexAudioPlaybackSetting() {}
324
329
 
@@ -188,7 +188,7 @@ class Simulated : SGCManager() {
188
188
  override fun dbg2() {}
189
189
 
190
190
  // Network Management
191
- override fun requestWifiScan() {
191
+ override fun requestWifiScan(scanId: String?) {
192
192
  Bridge.log("requestWifiScan")
193
193
  }
194
194
 
@@ -0,0 +1,566 @@
1
+ package com.mentra.bluetoothsdk.sgcs.ar99.ota;
2
+
3
+ import android.bluetooth.BluetoothGatt;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
6
+ import android.util.Log;
7
+ import java.io.ByteArrayOutputStream;
8
+ import java.io.File;
9
+ import java.io.FileInputStream;
10
+ import java.io.IOException;
11
+ import java.util.Arrays;
12
+
13
+ public class Ar99OtaManager {
14
+ private static final String TAG = "Ar99OtaManager";
15
+ private static final int DEFAULT_BLOCK_SIZE = 200;
16
+ private static final int TIMEOUT_MS = 5000;
17
+ private static final int RECONNECT_TIMEOUT_MS = 90000;
18
+
19
+ private static volatile Ar99OtaManager instance;
20
+
21
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
22
+ private OtaGattTransport transport;
23
+ private OTACallback callback;
24
+ private Runnable timeoutRunnable;
25
+ private Runnable reconnectTimeoutRunnable;
26
+
27
+ private int state = OtaCommandConstants.OTA.State.IDLE;
28
+ private byte[] firmwareData;
29
+ private int firmwareSize;
30
+ private long firmwareCrc32;
31
+ private int blockSize = DEFAULT_BLOCK_SIZE;
32
+ private int currentOffset = 0;
33
+ private boolean otaNotifyEnabled = false;
34
+
35
+ private Ar99OtaManager() {}
36
+
37
+ public static Ar99OtaManager getInstance() {
38
+ if (instance == null) {
39
+ synchronized (Ar99OtaManager.class) {
40
+ if (instance == null) instance = new Ar99OtaManager();
41
+ }
42
+ }
43
+ return instance;
44
+ }
45
+
46
+ public synchronized void setTransport(OtaGattTransport transport) {
47
+ this.transport = transport;
48
+ }
49
+
50
+ public synchronized void setCallback(OTACallback callback) {
51
+ this.callback = callback;
52
+ }
53
+
54
+ public synchronized int getState() {
55
+ return state;
56
+ }
57
+
58
+ public synchronized boolean isOTAInProgress() {
59
+ return state != OtaCommandConstants.OTA.State.IDLE
60
+ && state != OtaCommandConstants.OTA.State.COMPLETED
61
+ && state != OtaCommandConstants.OTA.State.FAILED;
62
+ }
63
+
64
+ public synchronized boolean isPausedWaitingReconnect() {
65
+ return state == OtaCommandConstants.OTA.State.PAUSED_DISCONNECTED;
66
+ }
67
+
68
+ public synchronized int getProgress() {
69
+ if (firmwareSize <= 0) return 0;
70
+ return (int) ((currentOffset * 100L) / firmwareSize);
71
+ }
72
+
73
+ public boolean startOTA(File firmwareFile) {
74
+ if (firmwareFile == null || !firmwareFile.exists()) {
75
+ notifyError(OtaCommandConstants.OTA.ErrorCode.UNKNOWN_ERROR, "Firmware file does not exist");
76
+ return false;
77
+ }
78
+ try {
79
+ return startOTA(readFile(firmwareFile));
80
+ } catch (IOException e) {
81
+ notifyError(OtaCommandConstants.OTA.ErrorCode.UNKNOWN_ERROR, "Failed to read firmware file");
82
+ return false;
83
+ }
84
+ }
85
+
86
+ public synchronized boolean startOTA(byte[] data) {
87
+ if (isOTAInProgress()) {
88
+ Log.w(TAG, "OTA is already in progress");
89
+ return false;
90
+ }
91
+ if (data == null || data.length == 0) {
92
+ notifyError(OtaCommandConstants.OTA.ErrorCode.PACKAGE_SIZE_ERROR, "Firmware data is empty");
93
+ return false;
94
+ }
95
+
96
+ state = OtaCommandConstants.OTA.State.IDLE;
97
+ otaNotifyEnabled = false;
98
+ cancelTimeout();
99
+ cancelReconnectTimeout();
100
+ firmwareData = data;
101
+ firmwareSize = data.length;
102
+ firmwareCrc32 = OtaCrc32Util.calculate(data);
103
+ blockSize = DEFAULT_BLOCK_SIZE;
104
+ currentOffset = 0;
105
+
106
+ OtaGattTransport t = transport;
107
+ if (t == null) {
108
+ notifyError(OtaCommandConstants.OTA.ErrorCode.UNKNOWN_ERROR, "OTA transport is not initialized");
109
+ return false;
110
+ }
111
+ if (!t.enableOtaNotification()) {
112
+ notifyError(OtaCommandConstants.OTA.ErrorCode.UNKNOWN_ERROR, "OTA service was not found");
113
+ return false;
114
+ }
115
+ return true;
116
+ }
117
+
118
+ public void cancelOTA() {
119
+ synchronized (this) {
120
+ state = OtaCommandConstants.OTA.State.IDLE;
121
+ otaNotifyEnabled = false;
122
+ firmwareData = null;
123
+ firmwareSize = 0;
124
+ currentOffset = 0;
125
+ cancelTimeout();
126
+ cancelReconnectTimeout();
127
+ }
128
+ OTACallback cb = callback;
129
+ if (cb != null) mainHandler.post(cb::onCancelled);
130
+ cleanupFirmware();
131
+ }
132
+
133
+ public void onBleDisconnected() {
134
+ boolean shouldNotify = false;
135
+ synchronized (this) {
136
+ if (!isOTAInProgress() || firmwareData == null || firmwareSize <= 0) return;
137
+ if (state == OtaCommandConstants.OTA.State.PAUSED_DISCONNECTED) return;
138
+ state = OtaCommandConstants.OTA.State.PAUSED_DISCONNECTED;
139
+ otaNotifyEnabled = false;
140
+ cancelTimeout();
141
+ startReconnectTimeout();
142
+ shouldNotify = true;
143
+ }
144
+ if (shouldNotify) notifyPausedWaitingReconnect();
145
+ }
146
+
147
+ public void tryResumeOtaAfterGattReady() {
148
+ boolean resume;
149
+ synchronized (this) {
150
+ resume =
151
+ state == OtaCommandConstants.OTA.State.PAUSED_DISCONNECTED
152
+ && firmwareData != null
153
+ && firmwareSize > 0;
154
+ }
155
+ if (resume) {
156
+ OtaGattTransport t = transport;
157
+ if (t != null) t.enableOtaNotification();
158
+ }
159
+ }
160
+
161
+ public void onOtaNotifyEnabled() {
162
+ synchronized (this) {
163
+ otaNotifyEnabled = true;
164
+ if (firmwareData == null || firmwareSize <= 0) return;
165
+ if (state != OtaCommandConstants.OTA.State.IDLE
166
+ && state != OtaCommandConstants.OTA.State.PAUSED_DISCONNECTED) {
167
+ return;
168
+ }
169
+ }
170
+ mainHandler.postDelayed(
171
+ () -> {
172
+ synchronized (Ar99OtaManager.this) {
173
+ if (state != OtaCommandConstants.OTA.State.IDLE
174
+ && state != OtaCommandConstants.OTA.State.PAUSED_DISCONNECTED) {
175
+ return;
176
+ }
177
+ }
178
+ sendUpgradeRequest();
179
+ },
180
+ 1000);
181
+ }
182
+
183
+ public boolean handleGattMtuChangedForOta(int status) {
184
+ synchronized (this) {
185
+ if (state != OtaCommandConstants.OTA.State.REQUESTING) return false;
186
+ }
187
+ if (status != BluetoothGatt.GATT_SUCCESS) {
188
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.UNKNOWN_ERROR, "MTU negotiation failed");
189
+ return true;
190
+ }
191
+ onMtuNegotiated();
192
+ return true;
193
+ }
194
+
195
+ public void handleOTAResponse(byte[] data) {
196
+ if (data == null || data.length < 5) return;
197
+ OtaProtocol.FrameHeader header = OtaProtocol.parseFrameHeader(data);
198
+ if (header == null || header.svcId != OtaCommandConstants.OTA.SVC_ID) return;
199
+ cancelTimeout();
200
+
201
+ byte[] payload = null;
202
+ if (header.paramLen > 0 && data.length >= header.payloadOffset + header.paramLen) {
203
+ payload = Arrays.copyOfRange(data, header.payloadOffset, header.payloadOffset + header.paramLen);
204
+ }
205
+
206
+ switch (header.cmd) {
207
+ case OtaCommandConstants.OTA.REQUEST_UPGRADE:
208
+ handleUpgradeRequestResponse(payload);
209
+ break;
210
+ case OtaCommandConstants.OTA.CONNECT_NEGOTIATION:
211
+ handleNegotiationResponse(payload);
212
+ break;
213
+ case OtaCommandConstants.OTA.NEGOTIATION_RESULT:
214
+ handleNegotiationResultResponse();
215
+ break;
216
+ case OtaCommandConstants.OTA.REQUIRE_IMAGE_DATA:
217
+ handleRequireImageData(payload);
218
+ break;
219
+ case OtaCommandConstants.OTA.SEND_IMAGE_DATA:
220
+ handleImageDataResponse(payload);
221
+ break;
222
+ case OtaCommandConstants.OTA.VALIDATE_IMAGE:
223
+ handleValidateImageResponse(payload);
224
+ break;
225
+ default:
226
+ startTimeout();
227
+ break;
228
+ }
229
+ }
230
+
231
+ private boolean sendUpgradeRequest() {
232
+ if (!isBluetoothConnected()) {
233
+ notifyError(OtaCommandConstants.OTA.ErrorCode.UNKNOWN_ERROR, "Bluetooth is not connected");
234
+ return false;
235
+ }
236
+ synchronized (this) {
237
+ if (firmwareData == null || firmwareSize <= 0) {
238
+ state = OtaCommandConstants.OTA.State.IDLE;
239
+ return false;
240
+ }
241
+ cancelReconnectTimeout();
242
+ state = OtaCommandConstants.OTA.State.REQUESTING;
243
+ }
244
+ OtaGattTransport t = transport;
245
+ if (t == null) {
246
+ notifyError(OtaCommandConstants.OTA.ErrorCode.UNKNOWN_ERROR, "OTA transport is not initialized");
247
+ return false;
248
+ }
249
+ t.requestMtu(247);
250
+ startTimeout();
251
+ return true;
252
+ }
253
+
254
+ private void onMtuNegotiated() {
255
+ mainHandler.postDelayed(
256
+ () -> {
257
+ synchronized (Ar99OtaManager.this) {
258
+ if (state != OtaCommandConstants.OTA.State.REQUESTING) return;
259
+ }
260
+ sendOtaData(OtaProtocol.buildRequestUpgrade(firmwareSize, blockSize, firmwareCrc32));
261
+ },
262
+ 500);
263
+ }
264
+
265
+ private void handleUpgradeRequestResponse(byte[] payload) {
266
+ if (payload == null || payload.length < 3) {
267
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.PROTOCOL_ERROR, "Invalid upgrade response");
268
+ return;
269
+ }
270
+
271
+ int offset = 0;
272
+ boolean success = false;
273
+ while (offset + 3 <= payload.length) {
274
+ int type = payload[offset++] & 0xFF;
275
+ int len = OtaByteUtils.readShortLE(payload, offset);
276
+ offset += 2;
277
+ if (offset + len > payload.length) {
278
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.PROTOCOL_ERROR, "Invalid upgrade TLV");
279
+ return;
280
+ }
281
+ if (type == 0x7F && len >= 4) {
282
+ int errorCode = OtaByteUtils.readIntLE(payload, offset);
283
+ if (errorCode == 100000) {
284
+ success = true;
285
+ } else {
286
+ handleOTAFailure(
287
+ OtaCommandConstants.OTA.ErrorCode.PROTOCOL_ERROR, "Upgrade request failed: " + errorCode);
288
+ return;
289
+ }
290
+ }
291
+ offset += len;
292
+ }
293
+
294
+ if (!success) {
295
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.PROTOCOL_ERROR, "Upgrade request was rejected");
296
+ return;
297
+ }
298
+ synchronized (this) {
299
+ state = OtaCommandConstants.OTA.State.NEGOTIATING;
300
+ }
301
+ sendOtaData(OtaProtocol.buildConnectNegotiation(blockSize));
302
+ startTimeout();
303
+ }
304
+
305
+ private void handleNegotiationResponse(byte[] payload) {
306
+ if (payload == null || payload.length < 3) {
307
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.PROTOCOL_ERROR, "Invalid negotiation response");
308
+ return;
309
+ }
310
+ int offset = 0;
311
+ while (offset + 3 <= payload.length) {
312
+ int type = payload[offset++] & 0xFF;
313
+ int len = OtaByteUtils.readShortLE(payload, offset);
314
+ offset += 2;
315
+ if (offset + len > payload.length) {
316
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.PROTOCOL_ERROR, "Invalid negotiation TLV");
317
+ return;
318
+ }
319
+ if (type == 0x03 && len >= 2) {
320
+ int suggestedBlockSize = OtaByteUtils.readShortLE(payload, offset);
321
+ if (suggestedBlockSize > 0) blockSize = suggestedBlockSize;
322
+ }
323
+ offset += len;
324
+ }
325
+ synchronized (this) {
326
+ state = OtaCommandConstants.OTA.State.WAITING_READY;
327
+ }
328
+ sendOtaData(OtaProtocol.buildNegotiationResult());
329
+ startTimeout();
330
+ }
331
+
332
+ private void handleNegotiationResultResponse() {
333
+ synchronized (this) {
334
+ state = OtaCommandConstants.OTA.State.TRANSFERRING;
335
+ }
336
+ startTimeout();
337
+ }
338
+
339
+ private void handleRequireImageData(byte[] payload) {
340
+ if (payload == null || payload.length < 3) {
341
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.PROTOCOL_ERROR, "Invalid image data request");
342
+ return;
343
+ }
344
+
345
+ int offset = 0;
346
+ int fileOffset = 0;
347
+ int fileLength = 0;
348
+ while (offset + 3 <= payload.length) {
349
+ int type = payload[offset++] & 0xFF;
350
+ int len = OtaByteUtils.readShortLE(payload, offset);
351
+ offset += 2;
352
+ if (offset + len > payload.length) {
353
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.PROTOCOL_ERROR, "Invalid image data TLV");
354
+ return;
355
+ }
356
+ if (type == 0x01 && len >= 4) {
357
+ fileOffset = OtaByteUtils.readIntLE(payload, offset);
358
+ } else if (type == 0x02 && len >= 4) {
359
+ fileLength = OtaByteUtils.readIntLE(payload, offset);
360
+ }
361
+ offset += len;
362
+ }
363
+
364
+ byte[] image;
365
+ int imageSize;
366
+ synchronized (this) {
367
+ image = firmwareData;
368
+ imageSize = firmwareSize;
369
+ }
370
+ if (image == null || imageSize <= 0 || fileOffset < 0 || fileOffset > imageSize) {
371
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.PROTOCOL_ERROR, "Invalid firmware offset");
372
+ return;
373
+ }
374
+ if (fileOffset >= imageSize) {
375
+ startTimeout();
376
+ return;
377
+ }
378
+
379
+ int remaining = imageSize - fileOffset;
380
+ int totalSendLength = fileLength > 0 ? Math.min(remaining, fileLength) : remaining;
381
+ int sentLength = 0;
382
+ int psn = 0;
383
+ while (sentLength < totalSendLength) {
384
+ int size = Math.min(totalSendLength - sentLength, blockSize);
385
+ int current = fileOffset + sentLength;
386
+ byte[] block = Arrays.copyOfRange(image, current, current + size);
387
+ sendOtaData(OtaProtocol.buildSendImageData(psn, block));
388
+ sentLength += size;
389
+ psn++;
390
+ }
391
+
392
+ synchronized (this) {
393
+ currentOffset = fileOffset + sentLength;
394
+ }
395
+ notifyProgress(currentOffset, getProgress());
396
+ startTimeout();
397
+ }
398
+
399
+ private void handleValidateImageResponse(byte[] payload) {
400
+ Integer validFlag = null;
401
+ if (payload != null && payload.length >= 4) {
402
+ int offset = 0;
403
+ while (offset + 3 <= payload.length) {
404
+ int type = payload[offset++] & 0xFF;
405
+ int len = OtaByteUtils.readShortLE(payload, offset);
406
+ offset += 2;
407
+ if (offset + len > payload.length) break;
408
+ if (type == 0x01 && len >= 1) {
409
+ validFlag = payload[offset] & 0xFF;
410
+ break;
411
+ }
412
+ offset += len;
413
+ }
414
+ }
415
+ if (validFlag == null && payload != null && payload.length > 0) {
416
+ validFlag = payload[0] & 0xFF;
417
+ }
418
+
419
+ if (validFlag != null && validFlag == 1) {
420
+ handleOTASuccess();
421
+ } else {
422
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.FIRMWARE_VALIDATION_FAILED, "Firmware validation failed");
423
+ }
424
+ }
425
+
426
+ private void handleImageDataResponse(byte[] payload) {
427
+ if (payload == null || payload.length < 5) {
428
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.PROTOCOL_ERROR, "Invalid image data response");
429
+ return;
430
+ }
431
+ int receivedOffset = OtaByteUtils.readIntLE(payload, 0);
432
+ int status = payload[4] & 0xFF;
433
+ if (status != 0) {
434
+ handleOTAFailure(status, "Firmware data write failed");
435
+ return;
436
+ }
437
+ synchronized (this) {
438
+ if (receivedOffset != currentOffset) currentOffset = receivedOffset;
439
+ }
440
+ startTimeout();
441
+ }
442
+
443
+ private void sendOtaData(byte[] frame) {
444
+ OtaGattTransport t = transport;
445
+ if (t == null) {
446
+ handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.UNKNOWN_ERROR, "OTA transport is not initialized");
447
+ return;
448
+ }
449
+ t.sendOtaData(frame);
450
+ }
451
+
452
+ private boolean isBluetoothConnected() {
453
+ OtaGattTransport t = transport;
454
+ return t != null && t.isBleConnected() && otaNotifyEnabled;
455
+ }
456
+
457
+ private void handleOTASuccess() {
458
+ synchronized (this) {
459
+ state = OtaCommandConstants.OTA.State.COMPLETED;
460
+ currentOffset = firmwareSize;
461
+ cancelTimeout();
462
+ cancelReconnectTimeout();
463
+ }
464
+ notifyProgress(firmwareSize, 100);
465
+ OTACallback cb = callback;
466
+ if (cb != null) mainHandler.post(() -> cb.onCompleted(false));
467
+ cleanupFirmware();
468
+ }
469
+
470
+ private void handleOTAFailure(int code, String message) {
471
+ synchronized (this) {
472
+ state = OtaCommandConstants.OTA.State.FAILED;
473
+ cancelTimeout();
474
+ cancelReconnectTimeout();
475
+ }
476
+ notifyError(code, message);
477
+ cleanupFirmware();
478
+ }
479
+
480
+ private synchronized void startTimeout() {
481
+ cancelTimeout();
482
+ timeoutRunnable =
483
+ () -> handleOTAFailure(OtaCommandConstants.OTA.ErrorCode.UNKNOWN_ERROR, "OTA timed out");
484
+ mainHandler.postDelayed(timeoutRunnable, TIMEOUT_MS);
485
+ }
486
+
487
+ private synchronized void cancelTimeout() {
488
+ if (timeoutRunnable != null) {
489
+ mainHandler.removeCallbacks(timeoutRunnable);
490
+ timeoutRunnable = null;
491
+ }
492
+ }
493
+
494
+ private synchronized void startReconnectTimeout() {
495
+ cancelReconnectTimeout();
496
+ reconnectTimeoutRunnable =
497
+ () -> {
498
+ synchronized (Ar99OtaManager.this) {
499
+ if (state != OtaCommandConstants.OTA.State.PAUSED_DISCONNECTED) return;
500
+ }
501
+ handleOTAFailure(
502
+ OtaCommandConstants.OTA.ErrorCode.UNKNOWN_ERROR, "OTA reconnect timed out");
503
+ };
504
+ mainHandler.postDelayed(reconnectTimeoutRunnable, RECONNECT_TIMEOUT_MS);
505
+ }
506
+
507
+ private synchronized void cancelReconnectTimeout() {
508
+ if (reconnectTimeoutRunnable != null) {
509
+ mainHandler.removeCallbacks(reconnectTimeoutRunnable);
510
+ reconnectTimeoutRunnable = null;
511
+ }
512
+ }
513
+
514
+ private void notifyProgress(int offset, int progress) {
515
+ OTACallback cb = callback;
516
+ int total;
517
+ synchronized (this) {
518
+ total = firmwareSize;
519
+ }
520
+ if (cb != null) mainHandler.post(() -> cb.onProgress(offset, total, progress));
521
+ }
522
+
523
+ private void notifyError(int errorCode, String message) {
524
+ OTACallback cb = callback;
525
+ if (cb != null) mainHandler.post(() -> cb.onError(errorCode, message));
526
+ }
527
+
528
+ private void notifyPausedWaitingReconnect() {
529
+ OTACallback cb = callback;
530
+ if (cb != null) mainHandler.post(cb::onPausedWaitingReconnect);
531
+ }
532
+
533
+ private static byte[] readFile(File file) throws IOException {
534
+ try (FileInputStream input = new FileInputStream(file);
535
+ ByteArrayOutputStream output = new ByteArrayOutputStream()) {
536
+ byte[] buffer = new byte[8192];
537
+ int read;
538
+ while ((read = input.read(buffer)) != -1) {
539
+ output.write(buffer, 0, read);
540
+ }
541
+ return output.toByteArray();
542
+ }
543
+ }
544
+
545
+ private synchronized void cleanupFirmware() {
546
+ firmwareData = null;
547
+ firmwareSize = 0;
548
+ firmwareCrc32 = 0;
549
+ currentOffset = 0;
550
+ blockSize = DEFAULT_BLOCK_SIZE;
551
+ otaNotifyEnabled = false;
552
+ cancelTimeout();
553
+ }
554
+
555
+ public interface OTACallback {
556
+ void onProgress(int offset, int total, int progress);
557
+
558
+ void onCompleted(boolean needReboot);
559
+
560
+ void onError(int errorCode, String message);
561
+
562
+ void onCancelled();
563
+
564
+ default void onPausedWaitingReconnect() {}
565
+ }
566
+ }
@@ -0,0 +1,18 @@
1
+ package com.mentra.bluetoothsdk.sgcs.ar99.ota;
2
+
3
+ public final class OtaByteUtils {
4
+ private OtaByteUtils() {}
5
+
6
+ public static int readShortLE(byte[] data, int offset) {
7
+ if (data == null || offset < 0 || offset + 1 >= data.length) return 0;
8
+ return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
9
+ }
10
+
11
+ public static int readIntLE(byte[] data, int offset) {
12
+ if (data == null || offset < 0 || offset + 3 >= data.length) return 0;
13
+ return (data[offset] & 0xFF)
14
+ | ((data[offset + 1] & 0xFF) << 8)
15
+ | ((data[offset + 2] & 0xFF) << 16)
16
+ | ((data[offset + 3] & 0xFF) << 24);
17
+ }
18
+ }
@@ -0,0 +1,35 @@
1
+ package com.mentra.bluetoothsdk.sgcs.ar99.ota;
2
+
3
+ public final class OtaCommandConstants {
4
+ private OtaCommandConstants() {}
5
+
6
+ public static final class OTA {
7
+ public static final byte SVC_ID = 0x09;
8
+
9
+ public static final byte REQUEST_UPGRADE = 0x01;
10
+ public static final byte CONNECT_NEGOTIATION = 0x02;
11
+ public static final byte REQUIRE_IMAGE_DATA = 0x03;
12
+ public static final byte VALIDATE_IMAGE = 0x06;
13
+ public static final byte NEGOTIATION_RESULT = 0x09;
14
+ public static final byte SEND_IMAGE_DATA = 0x0B;
15
+
16
+ public static final class ErrorCode {
17
+ public static final int PROTOCOL_ERROR = 0x04;
18
+ public static final int PACKAGE_SIZE_ERROR = 0x06;
19
+ public static final int FIRMWARE_VALIDATION_FAILED = 0x0A;
20
+ public static final int UNKNOWN_ERROR = 0xFF;
21
+ }
22
+
23
+ public static final class State {
24
+ public static final int IDLE = 0;
25
+ public static final int REQUESTING = 1;
26
+ public static final int NEGOTIATING = 2;
27
+ public static final int WAITING_READY = 3;
28
+ public static final int TRANSFERRING = 4;
29
+ public static final int VALIDATING = 5;
30
+ public static final int COMPLETED = 6;
31
+ public static final int FAILED = 7;
32
+ public static final int PAUSED_DISCONNECTED = 8;
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,13 @@
1
+ package com.mentra.bluetoothsdk.sgcs.ar99.ota;
2
+
3
+ import java.util.zip.CRC32;
4
+
5
+ public final class OtaCrc32Util {
6
+ private OtaCrc32Util() {}
7
+
8
+ public static long calculate(byte[] data) {
9
+ CRC32 crc32 = new CRC32();
10
+ if (data != null) crc32.update(data);
11
+ return crc32.getValue();
12
+ }
13
+ }