@mentra/bluetooth-sdk 0.1.18 → 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.
Files changed (52) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +119 -16
  3. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +0 -6
  4. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +22 -21
  5. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
  6. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
  7. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +457 -31
  8. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +361 -67
  10. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +7501 -3704
  11. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +134 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
  13. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +4 -1
  15. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleWireProtocol.java +108 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +4 -3
  17. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900LengthCodec.java +115 -0
  18. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +103 -68
  19. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +106 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +93 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +111 -11
  22. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +7 -9
  23. package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +17 -1
  24. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
  25. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +158 -0
  26. package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
  27. package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
  28. package/build/BluetoothSdk.types.d.ts +12 -1
  29. package/build/BluetoothSdk.types.d.ts.map +1 -1
  30. package/build/BluetoothSdk.types.js.map +1 -1
  31. package/build/types/index.d.ts +3 -0
  32. package/build/types/index.d.ts.map +1 -0
  33. package/build/types/index.js +2 -0
  34. package/build/types/index.js.map +1 -0
  35. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  36. package/ios/Source/DeviceManager.swift +134 -29
  37. package/ios/Source/DeviceStore.swift +0 -5
  38. package/ios/Source/MentraBluetoothSDK.swift +26 -23
  39. package/ios/Source/OtaManifest.swift +1 -1
  40. package/ios/Source/sgcs/G2.swift +518 -11
  41. package/ios/Source/sgcs/MentraLive.swift +308 -30
  42. package/ios/Source/sgcs/MentraNex.swift +416 -32
  43. package/ios/Source/sgcs/SGCManager.swift +155 -0
  44. package/ios/Source/sgcs/mentraos_ble.pb.swift +726 -122
  45. package/ios/Source/status/DeviceStatus.swift +0 -8
  46. package/ios/Source/utils/BleJsonCompact.swift +395 -0
  47. package/ios/Source/utils/BleWireProtocol.swift +92 -0
  48. package/ios/Source/utils/MessageChunkReassembler.swift +82 -0
  49. package/ios/Source/utils/MessageChunker.swift +73 -0
  50. package/package.json +14 -7
  51. package/src/BluetoothSdk.types.ts +13 -1
  52. package/src/types/index.ts +7 -0
@@ -0,0 +1,274 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import android.bluetooth.BluetoothDevice
4
+ import android.bluetooth.BluetoothSocket
5
+ import android.os.Build
6
+ import android.util.Log
7
+ import androidx.annotation.RequiresApi
8
+ import com.mentra.bluetoothsdk.Bridge
9
+ import com.mentra.bluetoothsdk.utils.K900ProtocolUtils
10
+ import java.io.IOException
11
+ import java.util.concurrent.atomic.AtomicBoolean
12
+
13
+ /**
14
+ * L2CAP CoC (connection-oriented channel) fast path for Mentra Live file transfers.
15
+ *
16
+ * New BES2700 firmware registers an LE L2CAP CoC server on PSM 0x00C9. When the phone opens that
17
+ * channel, the glasses send BLE photo file packets over it INSTEAD of GATT FILE_READ notifications.
18
+ * The packet framing over the channel is identical to the GATT file notifications (K900 file
19
+ * frames): ##(2) + type(1) + packSize(2, BE) + packIndex(2) + fileSize(4) + fileName(16) + flags(2)
20
+ * + data(packSize) + verify(1) + $$(2).
21
+ *
22
+ * This class only READS from the channel. All phone→glasses traffic stays on GATT. If the channel
23
+ * can't be opened (older firmware without the L2CAP server, connect timeout, etc.) we log once and
24
+ * do nothing else — GATT notifications remain the default file path.
25
+ *
26
+ * Complete frames are handed to [onFileFrame] on the read thread; the caller feeds them into the
27
+ * exact same processing path used for GATT file notifications so downstream reassembly
28
+ * (FileTransferSession, transfer_complete) is unchanged.
29
+ */
30
+ class MentraLiveL2capChannel(
31
+ private val psm: Int,
32
+ private val onFileFrame: (ByteArray) -> Unit
33
+ ) {
34
+
35
+ companion object {
36
+ private const val TAG = "LiveL2cap"
37
+
38
+ // BluetoothSocket.connect() has no timeout parameter; a watchdog closes the socket to
39
+ // abort a stuck connect after this long.
40
+ private const val CONNECT_TIMEOUT_MS = 3000L
41
+
42
+ private const val READ_CHUNK_SIZE = 4096
43
+ private const val MAX_BUFFER_SIZE = 64 * 1024
44
+
45
+ // K900 file frame: ##(2) + type(1) + packSize(2) + packIndex(2) + fileSize(4) +
46
+ // fileName(16) + flags(2) + data(packSize) + verify(1) + $$(2)
47
+ // Total frame size = FRAME_OVERHEAD + packSize.
48
+ private const val FRAME_HEADER_MIN = 5 // bytes needed to read packSize: ## + type + size
49
+ private const val FRAME_OVERHEAD = 2 + 1 + 2 + 2 + 4 + 16 + 2 + 1 + 2 // = 32
50
+ }
51
+
52
+ @Volatile private var socket: BluetoothSocket? = null
53
+ @Volatile private var closed = false
54
+
55
+ /** True while the channel is connected and the read loop is running. */
56
+ @Volatile
57
+ var isOpen = false
58
+ private set
59
+
60
+ /**
61
+ * Open the channel to [device] and start the read loop on a background thread. Never throws;
62
+ * failures are logged and leave the GATT path untouched.
63
+ */
64
+ @RequiresApi(Build.VERSION_CODES.Q)
65
+ fun open(device: BluetoothDevice) {
66
+ val thread = Thread({ runChannel(device) }, "MentraLive-L2CAP")
67
+ thread.isDaemon = true
68
+ thread.start()
69
+ }
70
+
71
+ /** Close the channel (call on disconnect). Safe to call multiple times / while connecting. */
72
+ fun close() {
73
+ closed = true
74
+ isOpen = false
75
+ closeSocketQuietly()
76
+ }
77
+
78
+ @RequiresApi(Build.VERSION_CODES.Q)
79
+ private fun runChannel(device: BluetoothDevice) {
80
+ val sock: BluetoothSocket
81
+ try {
82
+ sock = device.createInsecureL2capChannel(psm)
83
+ } catch (e: Exception) { // IOException or SecurityException
84
+ logUnavailable(e)
85
+ return
86
+ }
87
+ socket = sock
88
+
89
+ if (closed) {
90
+ closeSocketQuietly()
91
+ return
92
+ }
93
+
94
+ // Watchdog: closing the socket makes a stuck connect() throw an IOException.
95
+ val connectDone = AtomicBoolean(false)
96
+ val watchdog =
97
+ Thread(
98
+ {
99
+ try {
100
+ Thread.sleep(CONNECT_TIMEOUT_MS)
101
+ } catch (ie: InterruptedException) {
102
+ return@Thread // connect finished in time
103
+ }
104
+ if (!connectDone.get()) {
105
+ Log.w(TAG, "L2CAP: connect timed out after " + CONNECT_TIMEOUT_MS + "ms")
106
+ closeSocketQuietly()
107
+ }
108
+ },
109
+ "MentraLive-L2CAP-Watchdog"
110
+ )
111
+ watchdog.isDaemon = true
112
+ watchdog.start()
113
+
114
+ try {
115
+ sock.connect()
116
+ } catch (e: Exception) { // IOException or SecurityException
117
+ connectDone.set(true)
118
+ watchdog.interrupt()
119
+ if (!closed) {
120
+ logUnavailable(e)
121
+ }
122
+ closeSocketQuietly()
123
+ return
124
+ }
125
+ connectDone.set(true)
126
+ watchdog.interrupt()
127
+
128
+ if (closed) {
129
+ closeSocketQuietly()
130
+ return
131
+ }
132
+
133
+ isOpen = true
134
+ Bridge.log("LIVE: L2CAP: channel open (PSM 0xC9)")
135
+
136
+ readLoop(sock)
137
+ }
138
+
139
+ /**
140
+ * Accumulate bytes from the socket, extract complete K900 file frames, and dispatch each one
141
+ * via [onFileFrame].
142
+ */
143
+ private fun readLoop(sock: BluetoothSocket) {
144
+ val readBuf = ByteArray(READ_CHUNK_SIZE)
145
+ val acc = ByteArray(MAX_BUFFER_SIZE)
146
+ var accSize = 0
147
+
148
+ try {
149
+ val input = sock.inputStream
150
+ while (!closed) {
151
+ val n = input.read(readBuf)
152
+ if (n < 0) {
153
+ break // stream closed by peer
154
+ }
155
+ if (n == 0) {
156
+ continue
157
+ }
158
+ if (accSize + n > acc.size) {
159
+ Log.e(TAG, "L2CAP: reassembly buffer overflow, clearing buffer")
160
+ accSize = 0
161
+ if (n > acc.size) {
162
+ continue
163
+ }
164
+ }
165
+ System.arraycopy(readBuf, 0, acc, accSize, n)
166
+ accSize += n
167
+ accSize = extractAndDispatchFrames(acc, accSize)
168
+ }
169
+ } catch (e: IOException) {
170
+ if (!closed) {
171
+ Log.w(TAG, "L2CAP: read loop ended: " + e.message)
172
+ }
173
+ } finally {
174
+ isOpen = false
175
+ closeSocketQuietly()
176
+ if (!closed) {
177
+ Bridge.log("LIVE: L2CAP: channel closed")
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Extract complete K900 file frames from [buffer] (valid up to [size]) and dispatch them.
184
+ * Mirrors the framing rules of MentraLive.extractCompleteFilePackets: scan for the ## start
185
+ * marker, read packSize (bytes 3-4, big-endian) to compute the total frame size, wait for the
186
+ * full frame, and verify the $$ end marker. Returns the number of unconsumed bytes remaining
187
+ * at the front of [buffer].
188
+ */
189
+ private fun extractAndDispatchFrames(buffer: ByteArray, size: Int): Int {
190
+ var pos = 0
191
+
192
+ while (pos < size) {
193
+ // Find start marker ## (0x23 0x23)
194
+ var startPos = -1
195
+ for (i in pos until size - 1) {
196
+ if (buffer[i] == 0x23.toByte() && buffer[i + 1] == 0x23.toByte()) {
197
+ startPos = i
198
+ break
199
+ }
200
+ }
201
+
202
+ if (startPos < 0) {
203
+ // No start marker; drop everything except a possible trailing '#'
204
+ pos = if (size > 0 && buffer[size - 1] == 0x23.toByte()) size - 1 else size
205
+ break
206
+ }
207
+
208
+ pos = startPos
209
+
210
+ // Need ##(2) + type(1) + packSize(2) to know the frame size
211
+ if (size - pos < FRAME_HEADER_MIN) {
212
+ break // wait for more data
213
+ }
214
+
215
+ // packSize is big-endian at bytes 3-4 (same as the GATT reassembly path)
216
+ val packSize =
217
+ ((buffer[pos + 3].toInt() and 0xFF) shl 8) or (buffer[pos + 4].toInt() and 0xFF)
218
+
219
+ if (packSize < 0 || packSize > K900ProtocolUtils.FILE_PACK_SIZE) {
220
+ Log.w(TAG, "L2CAP: invalid packSize " + packSize + ", skipping start marker")
221
+ pos += 1
222
+ continue
223
+ }
224
+
225
+ val frameSize = FRAME_OVERHEAD + packSize
226
+ if (size - pos < frameSize) {
227
+ break // wait for the rest of the frame
228
+ }
229
+
230
+ // Verify end marker $$ at the expected position
231
+ val endMarkerPos = pos + frameSize - 2
232
+ if (buffer[endMarkerPos] != 0x24.toByte() || buffer[endMarkerPos + 1] != 0x24.toByte()) {
233
+ Log.w(
234
+ TAG,
235
+ "L2CAP: end marker $$ not found (packSize=" +
236
+ packSize +
237
+ "), skipping start marker"
238
+ )
239
+ pos += 1
240
+ continue
241
+ }
242
+
243
+ val frame = ByteArray(frameSize)
244
+ System.arraycopy(buffer, pos, frame, 0, frameSize)
245
+ try {
246
+ onFileFrame(frame)
247
+ } catch (t: Throwable) {
248
+ Log.e(TAG, "L2CAP: onFileFrame threw", t)
249
+ }
250
+
251
+ pos += frameSize
252
+ }
253
+
254
+ // Compact unconsumed bytes to the front of the buffer
255
+ val remaining = size - pos
256
+ if (remaining > 0 && pos > 0) {
257
+ System.arraycopy(buffer, pos, buffer, 0, remaining)
258
+ }
259
+ return if (remaining > 0) remaining else 0
260
+ }
261
+
262
+ private fun logUnavailable(e: Exception) {
263
+ Bridge.log("LIVE: L2CAP: unavailable, staying on GATT (" + e.javaClass.simpleName + ")")
264
+ }
265
+
266
+ private fun closeSocketQuietly() {
267
+ val sock = socket ?: return
268
+ try {
269
+ sock.close()
270
+ } catch (e: IOException) {
271
+ // ignore
272
+ }
273
+ }
274
+ }