@mentra/bluetooth-sdk 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +52 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +119 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +22 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +457 -31
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +361 -67
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +7501 -3704
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +134 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +0 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleWireProtocol.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +4 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900LengthCodec.java +115 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +103 -68
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +93 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +111 -11
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +7 -9
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +17 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +158 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
- package/build/BluetoothSdk.types.d.ts +12 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +2 -0
- package/build/types/index.js.map +1 -0
- package/ios/LocalPhotoUploadServer.swift +78 -0
- package/ios/MentraPhotoReceiverModule.swift +10 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +63 -0
- package/ios/Source/DeviceManager.swift +134 -29
- package/ios/Source/DeviceStore.swift +0 -5
- package/ios/Source/MentraBluetoothSDK.swift +26 -23
- package/ios/Source/OtaManifest.swift +1 -1
- package/ios/Source/internal/BleTraceLogger.swift +192 -0
- package/ios/Source/sgcs/G2.swift +518 -11
- package/ios/Source/sgcs/MentraLive.swift +607 -39
- package/ios/Source/sgcs/MentraNex.swift +416 -32
- package/ios/Source/sgcs/SGCManager.swift +155 -0
- package/ios/Source/sgcs/mentraos_ble.pb.swift +726 -122
- package/ios/Source/status/DeviceStatus.swift +0 -8
- package/ios/Source/utils/BleJsonCompact.swift +395 -0
- package/ios/Source/utils/BleWireProtocol.swift +92 -0
- package/ios/Source/utils/MessageChunkReassembler.swift +82 -0
- package/ios/Source/utils/MessageChunker.swift +73 -0
- package/package.json +14 -7
- package/src/BluetoothSdk.types.ts +13 -1
- package/src/types/index.ts +7 -0
|
@@ -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
|
+
}
|