@mentra/bluetooth-sdk 0.1.18 → 0.1.20
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/BluetoothSdkModule.kt +135 -51
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +120 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +34 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +488 -15
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +551 -159
- 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/types/DeviceModels.kt +2 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +493 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -5
- 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 +109 -73
- 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/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -9
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- 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/BlePhotoUploadServiceTest.java +9 -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 +37 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +1 -0
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -0
- package/build/index.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/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +136 -30
- package/ios/Source/DeviceStore.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +40 -24
- package/ios/Source/OtaManifest.swift +1 -1
- package/ios/Source/camera/CameraModels.swift +17 -3
- package/ios/Source/sgcs/G1.swift +1 -1
- package/ios/Source/sgcs/G2.swift +518 -11
- package/ios/Source/sgcs/MentraLive.swift +443 -42
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- 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/G1Text.swift +30 -1
- 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 +39 -1
- package/src/_private/BluetoothSdkModule.ts +4 -0
- package/src/_private/photoRequestPayload.ts +1 -0
- package/src/index.ts +6 -0
- package/src/types/index.ts +7 -0
|
@@ -25,6 +25,7 @@ import android.content.pm.PackageManager
|
|
|
25
25
|
import android.graphics.Bitmap
|
|
26
26
|
import android.os.Build
|
|
27
27
|
import android.os.Handler
|
|
28
|
+
import android.os.HandlerThread
|
|
28
29
|
import android.os.Looper
|
|
29
30
|
import android.util.Log
|
|
30
31
|
import androidx.core.app.ActivityCompat
|
|
@@ -41,6 +42,9 @@ import com.mentra.bluetoothsdk.utils.ConnTypes
|
|
|
41
42
|
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
42
43
|
import com.mentra.bluetoothsdk.utils.IncidentLogBleRelayNaming
|
|
43
44
|
import com.mentra.bluetoothsdk.utils.IncidentLogBleUploadService
|
|
45
|
+
import com.mentra.bluetoothsdk.utils.BleJsonCompact
|
|
46
|
+
import com.mentra.bluetoothsdk.utils.BleWireProtocol
|
|
47
|
+
import com.mentra.bluetoothsdk.utils.K900LengthCodec
|
|
44
48
|
import com.mentra.bluetoothsdk.utils.K900ProtocolUtils
|
|
45
49
|
import com.mentra.bluetoothsdk.utils.MessageChunkReassembler
|
|
46
50
|
import com.mentra.bluetoothsdk.utils.MessageChunker
|
|
@@ -90,6 +94,13 @@ class MentraLive : SGCManager() {
|
|
|
90
94
|
// LC3 frame size for Mentra Live
|
|
91
95
|
private const val LC3_FRAME_SIZE = 40
|
|
92
96
|
private const val VOICE_ACTIVITY_DETECTION_SWITCH_TYPE = 8
|
|
97
|
+
private const val BES2700_MTU_LIMIT = 509
|
|
98
|
+
|
|
99
|
+
// L2CAP CoC fast path: new BES2700 firmware registers an LE L2CAP CoC server on this
|
|
100
|
+
// PSM. When the phone opens the channel, the glasses send file packets over it instead
|
|
101
|
+
// of GATT FILE_READ notifications. Phone→glasses traffic stays on GATT.
|
|
102
|
+
private const val L2CAP_FILE_PSM = 0x00C9
|
|
103
|
+
private const val FILE_PACKET_LOG_INTERVAL = 32
|
|
93
104
|
|
|
94
105
|
// BLE UUIDs - updated to match K900 BES2800 MCU UUIDs for compatibility with both glass
|
|
95
106
|
// types
|
|
@@ -199,6 +210,15 @@ class MentraLive : SGCManager() {
|
|
|
199
210
|
|
|
200
211
|
// Local-only fields (not in parent SGCManager)
|
|
201
212
|
private var buildNumberInt = 0 // Build number as integer for version checks
|
|
213
|
+
private var peerWireProtocolVersion = 0
|
|
214
|
+
private var useBinaryWireProtocol = false
|
|
215
|
+
private var wireHandshakeQueued = false
|
|
216
|
+
// Negotiated K900 STRING length endianness for the phone<->glasses BLE link. Defaults to
|
|
217
|
+
// legacy big-endian; upgraded to little-endian only when the glasses advertise wire_caps.k900_le
|
|
218
|
+
// (or a v2 binary handshake succeeds, which implies wire-v2 LE).
|
|
219
|
+
private var peerK900Le = false
|
|
220
|
+
private var peerWireCapsBinary = false
|
|
221
|
+
private var peerFilePayloadV2 = false
|
|
202
222
|
// Note: appVersion, buildNumber, deviceModel, androidVersion
|
|
203
223
|
// are inherited from SGCManager parent class
|
|
204
224
|
|
|
@@ -225,6 +245,9 @@ class MentraLive : SGCManager() {
|
|
|
225
245
|
private var lc3ReadCharacteristic: BluetoothGattCharacteristic? = null
|
|
226
246
|
private var lc3WriteCharacteristic: BluetoothGattCharacteristic? = null
|
|
227
247
|
private var handler = Handler(Looper.getMainLooper())
|
|
248
|
+
private val fileProcessingThread =
|
|
249
|
+
HandlerThread("MentraLive-FileProcessing").apply { start() }
|
|
250
|
+
private val fileProcessingHandler = Handler(fileProcessingThread.looper)
|
|
228
251
|
private var scheduler: ScheduledExecutorService? = null
|
|
229
252
|
private var isScanning = false
|
|
230
253
|
private var isConnecting = false
|
|
@@ -304,7 +327,7 @@ class MentraLive : SGCManager() {
|
|
|
304
327
|
private var activeFileTransfers = ConcurrentHashMap<String, FileTransferSession>()
|
|
305
328
|
|
|
306
329
|
// BLE photo transfer tracking
|
|
307
|
-
private var blePhotoTransfers: MutableMap<String, BlePhotoTransfer> =
|
|
330
|
+
private var blePhotoTransfers: MutableMap<String, BlePhotoTransfer> = ConcurrentHashMap()
|
|
308
331
|
|
|
309
332
|
/** Expected incident log relay files from glasses (B… firmware, L… logcat). */
|
|
310
333
|
private val bleIncidentLogRelays = ConcurrentHashMap<String, BleIncidentLogRelay>()
|
|
@@ -319,6 +342,12 @@ class MentraLive : SGCManager() {
|
|
|
319
342
|
private var fileReadNotificationCount = 0 // Debug counter for FILE_READ notifications
|
|
320
343
|
private val incomingChunkReassembler = MessageChunkReassembler()
|
|
321
344
|
|
|
345
|
+
// L2CAP CoC fast path for incoming file transfers (see L2CAP_FILE_PSM).
|
|
346
|
+
// The channel is read-only; all outgoing messages remain on GATT. When it can't be
|
|
347
|
+
// opened (older firmware, Android < 10), GATT notifications remain the file path.
|
|
348
|
+
private val enableL2capFilePath = true
|
|
349
|
+
private var l2capFileChannel: MentraLiveL2capChannel? = null
|
|
350
|
+
|
|
322
351
|
private val connectionLock = Any()
|
|
323
352
|
|
|
324
353
|
private var glassesMediaVolumeTimeoutRunnable: Runnable? = null
|
|
@@ -752,6 +781,13 @@ class MentraLive : SGCManager() {
|
|
|
752
781
|
if (isEqual) {
|
|
753
782
|
if (state == ConnTypes.DISCONNECTED) {
|
|
754
783
|
incomingChunkReassembler.clear()
|
|
784
|
+
peerWireProtocolVersion = 0
|
|
785
|
+
useBinaryWireProtocol = false
|
|
786
|
+
wireHandshakeQueued = false
|
|
787
|
+
peerK900Le = false
|
|
788
|
+
peerWireCapsBinary = false
|
|
789
|
+
peerFilePayloadV2 = false
|
|
790
|
+
BleJsonCompact.resetSession()
|
|
755
791
|
}
|
|
756
792
|
return
|
|
757
793
|
}
|
|
@@ -780,6 +816,13 @@ class MentraLive : SGCManager() {
|
|
|
780
816
|
DeviceStore.apply("glasses", "signalStrength", -1)
|
|
781
817
|
DeviceStore.apply("glasses", "signalStrengthUpdatedAt", 0L)
|
|
782
818
|
incomingChunkReassembler.clear()
|
|
819
|
+
peerWireProtocolVersion = 0
|
|
820
|
+
useBinaryWireProtocol = false
|
|
821
|
+
wireHandshakeQueued = false
|
|
822
|
+
peerK900Le = false
|
|
823
|
+
peerWireCapsBinary = false
|
|
824
|
+
peerFilePayloadV2 = false
|
|
825
|
+
BleJsonCompact.resetSession()
|
|
783
826
|
// Drop OTA caches when fully disconnected — avoids leaking session/step state
|
|
784
827
|
// from a previous pairing into the next one.
|
|
785
828
|
resetOtaCache()
|
|
@@ -1405,6 +1448,15 @@ class MentraLive : SGCManager() {
|
|
|
1405
1448
|
)
|
|
1406
1449
|
}
|
|
1407
1450
|
|
|
1451
|
+
private fun phyLabel(phy: Int): String {
|
|
1452
|
+
return when (phy) {
|
|
1453
|
+
BluetoothDevice.PHY_LE_1M -> "1M"
|
|
1454
|
+
BluetoothDevice.PHY_LE_2M -> "2M"
|
|
1455
|
+
BluetoothDevice.PHY_LE_CODED -> "coded"
|
|
1456
|
+
else -> "unknown($phy)"
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1408
1460
|
/** GATT callback for BLE operations */
|
|
1409
1461
|
private val gattCallback: BluetoothGattCallback =
|
|
1410
1462
|
object : BluetoothGattCallback() {
|
|
@@ -1427,6 +1479,7 @@ class MentraLive : SGCManager() {
|
|
|
1427
1479
|
isConnecting = false
|
|
1428
1480
|
isConnected = true
|
|
1429
1481
|
connectedDevice = gatt.device
|
|
1482
|
+
|
|
1430
1483
|
DeviceStore.apply("glasses", "bluetoothName", connectedDevice!!.name)
|
|
1431
1484
|
// Persist MAC so reconnection can use direct GATT instead of scanning
|
|
1432
1485
|
if (connectedDevice!!.address != null) {
|
|
@@ -1523,6 +1576,11 @@ class MentraLive : SGCManager() {
|
|
|
1523
1576
|
// Stop micbeat mechanism
|
|
1524
1577
|
stopMicBeat()
|
|
1525
1578
|
|
|
1579
|
+
// Close the L2CAP file channel (if the fast path was open)
|
|
1580
|
+
closeL2capFileChannel()
|
|
1581
|
+
fileProcessingHandler.removeCallbacksAndMessages(null)
|
|
1582
|
+
clearFilePacketBuffer()
|
|
1583
|
+
|
|
1526
1584
|
// Clean up GATT resources
|
|
1527
1585
|
closeGattQuietly(false)
|
|
1528
1586
|
|
|
@@ -1575,6 +1633,11 @@ class MentraLive : SGCManager() {
|
|
|
1575
1633
|
// Stop micbeat mechanism
|
|
1576
1634
|
stopMicBeat()
|
|
1577
1635
|
|
|
1636
|
+
// Close the L2CAP file channel (if the fast path was open)
|
|
1637
|
+
closeL2capFileChannel()
|
|
1638
|
+
fileProcessingHandler.removeCallbacksAndMessages(null)
|
|
1639
|
+
clearFilePacketBuffer()
|
|
1640
|
+
|
|
1578
1641
|
// Clean up resources
|
|
1579
1642
|
closeGattQuietly(false)
|
|
1580
1643
|
|
|
@@ -1586,6 +1649,38 @@ class MentraLive : SGCManager() {
|
|
|
1586
1649
|
}
|
|
1587
1650
|
}
|
|
1588
1651
|
|
|
1652
|
+
override fun onPhyUpdate(
|
|
1653
|
+
gatt: BluetoothGatt,
|
|
1654
|
+
txPhy: Int,
|
|
1655
|
+
rxPhy: Int,
|
|
1656
|
+
status: Int
|
|
1657
|
+
) {
|
|
1658
|
+
Bridge.log(
|
|
1659
|
+
"LIVE: PHY update tx=" +
|
|
1660
|
+
phyLabel(txPhy) +
|
|
1661
|
+
", rx=" +
|
|
1662
|
+
phyLabel(rxPhy) +
|
|
1663
|
+
", status=" +
|
|
1664
|
+
status
|
|
1665
|
+
)
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
override fun onPhyRead(
|
|
1669
|
+
gatt: BluetoothGatt,
|
|
1670
|
+
txPhy: Int,
|
|
1671
|
+
rxPhy: Int,
|
|
1672
|
+
status: Int
|
|
1673
|
+
) {
|
|
1674
|
+
Bridge.log(
|
|
1675
|
+
"LIVE: PHY read tx=" +
|
|
1676
|
+
phyLabel(txPhy) +
|
|
1677
|
+
", rx=" +
|
|
1678
|
+
phyLabel(rxPhy) +
|
|
1679
|
+
", status=" +
|
|
1680
|
+
status
|
|
1681
|
+
)
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1589
1684
|
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
1590
1685
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
1591
1686
|
Bridge.log("LIVE: GATT services discovered")
|
|
@@ -2570,18 +2665,6 @@ class MentraLive : SGCManager() {
|
|
|
2570
2665
|
var iterations = 0
|
|
2571
2666
|
val MAX_ITERATIONS = 100
|
|
2572
2667
|
|
|
2573
|
-
// Debug: Log hex dump of first 40 bytes
|
|
2574
|
-
val hexFirst = StringBuilder()
|
|
2575
|
-
for (i in 0 until Math.min(40, filePacketBufferSize)) {
|
|
2576
|
-
hexFirst.append(String.format("%02X ", filePacketBuffer[i]))
|
|
2577
|
-
}
|
|
2578
|
-
Bridge.log(
|
|
2579
|
-
"LIVE: 📦 extractCompleteFilePackets: buffer has " +
|
|
2580
|
-
filePacketBufferSize +
|
|
2581
|
-
" bytes, first 40: " +
|
|
2582
|
-
hexFirst.toString()
|
|
2583
|
-
)
|
|
2584
|
-
|
|
2585
2668
|
while (pos < filePacketBufferSize && iterations++ < MAX_ITERATIONS) {
|
|
2586
2669
|
// Find start marker ## (0x23 0x23)
|
|
2587
2670
|
var startPos = -1
|
|
@@ -2616,11 +2699,6 @@ class MentraLive : SGCManager() {
|
|
|
2616
2699
|
|
|
2617
2700
|
// Need at least 5 bytes to read type and packSize: ## (2) + type (1) + packSize (2)
|
|
2618
2701
|
if (filePacketBufferSize - pos < 5) {
|
|
2619
|
-
Bridge.log(
|
|
2620
|
-
"LIVE: 📦 Not enough data for header, have " +
|
|
2621
|
-
(filePacketBufferSize - pos) +
|
|
2622
|
-
" bytes, need 5"
|
|
2623
|
-
)
|
|
2624
2702
|
break
|
|
2625
2703
|
}
|
|
2626
2704
|
|
|
@@ -2630,33 +2708,9 @@ class MentraLive : SGCManager() {
|
|
|
2630
2708
|
((filePacketBuffer[packSizeOffset].toInt() and 0xFF) shl 8) or
|
|
2631
2709
|
(filePacketBuffer[packSizeOffset + 1].toInt() and 0xFF)
|
|
2632
2710
|
|
|
2633
|
-
// Also try little-endian for comparison
|
|
2634
|
-
val packSizeLE =
|
|
2635
|
-
(filePacketBuffer[packSizeOffset].toInt() and 0xFF) or
|
|
2636
|
-
((filePacketBuffer[packSizeOffset + 1].toInt() and 0xFF) shl 8)
|
|
2637
|
-
Bridge.log(
|
|
2638
|
-
"LIVE: 📦 Header bytes 3-4: 0x" +
|
|
2639
|
-
String.format(
|
|
2640
|
-
"%02X%02X",
|
|
2641
|
-
filePacketBuffer[packSizeOffset],
|
|
2642
|
-
filePacketBuffer[packSizeOffset + 1]
|
|
2643
|
-
) +
|
|
2644
|
-
" -> packSize BE=" +
|
|
2645
|
-
packSize +
|
|
2646
|
-
", LE=" +
|
|
2647
|
-
packSizeLE
|
|
2648
|
-
)
|
|
2649
|
-
|
|
2650
2711
|
// Validate packSize
|
|
2651
2712
|
if (packSize < 0 || packSize > K900ProtocolUtils.FILE_PACK_SIZE) {
|
|
2652
|
-
Log.w(
|
|
2653
|
-
TAG,
|
|
2654
|
-
"Invalid packSize " +
|
|
2655
|
-
packSize +
|
|
2656
|
-
" (LE would be " +
|
|
2657
|
-
packSizeLE +
|
|
2658
|
-
"), skipping start marker"
|
|
2659
|
-
)
|
|
2713
|
+
Log.w(TAG, "Invalid packSize $packSize, skipping start marker")
|
|
2660
2714
|
pos = startPos + 1
|
|
2661
2715
|
continue
|
|
2662
2716
|
}
|
|
@@ -2669,17 +2723,7 @@ class MentraLive : SGCManager() {
|
|
|
2669
2723
|
// Check if we have the complete packet
|
|
2670
2724
|
val availableBytes = filePacketBufferSize - pos
|
|
2671
2725
|
if (availableBytes < expectedPacketSize) {
|
|
2672
|
-
|
|
2673
|
-
Bridge.log(
|
|
2674
|
-
"LIVE: 📦 Waiting for more data: have " +
|
|
2675
|
-
availableBytes +
|
|
2676
|
-
" of " +
|
|
2677
|
-
expectedPacketSize +
|
|
2678
|
-
" bytes (packSize=" +
|
|
2679
|
-
packSize +
|
|
2680
|
-
")"
|
|
2681
|
-
)
|
|
2682
|
-
break // IMPORTANT: break here, don't continue looking for end marker
|
|
2726
|
+
break
|
|
2683
2727
|
}
|
|
2684
2728
|
|
|
2685
2729
|
// Verify end marker $$ at expected position
|
|
@@ -2687,25 +2731,6 @@ class MentraLive : SGCManager() {
|
|
|
2687
2731
|
val endByte1 = filePacketBuffer[endMarkerPos]
|
|
2688
2732
|
val endByte2 = filePacketBuffer[endMarkerPos + 1]
|
|
2689
2733
|
|
|
2690
|
-
// Debug: Show bytes around expected end marker position
|
|
2691
|
-
val endContext = StringBuilder()
|
|
2692
|
-
for (i in
|
|
2693
|
-
Math.max(0, endMarkerPos - 5)..Math.min(
|
|
2694
|
-
filePacketBufferSize - 1,
|
|
2695
|
-
endMarkerPos + 5
|
|
2696
|
-
)) {
|
|
2697
|
-
if (i == endMarkerPos) endContext.append("[")
|
|
2698
|
-
endContext.append(String.format("%02X", filePacketBuffer[i]))
|
|
2699
|
-
if (i == endMarkerPos + 1) endContext.append("]")
|
|
2700
|
-
endContext.append(" ")
|
|
2701
|
-
}
|
|
2702
|
-
Bridge.log(
|
|
2703
|
-
"LIVE: 📦 End marker check at pos " +
|
|
2704
|
-
endMarkerPos +
|
|
2705
|
-
": " +
|
|
2706
|
-
endContext.toString()
|
|
2707
|
-
)
|
|
2708
|
-
|
|
2709
2734
|
if (endByte1 != 0x24.toByte() || endByte2 != 0x24.toByte()) {
|
|
2710
2735
|
// End marker not found - could be corrupted packet or wrong packSize interpretation
|
|
2711
2736
|
Log.w(
|
|
@@ -2730,22 +2755,10 @@ class MentraLive : SGCManager() {
|
|
|
2730
2755
|
val completePacket = ByteArray(expectedPacketSize)
|
|
2731
2756
|
System.arraycopy(filePacketBuffer, pos, completePacket, 0, expectedPacketSize)
|
|
2732
2757
|
|
|
2733
|
-
Bridge.log(
|
|
2734
|
-
"LIVE: 📦 ✅ Complete file packet reassembled: " + expectedPacketSize + " bytes"
|
|
2735
|
-
)
|
|
2736
|
-
|
|
2737
2758
|
// Process the complete packet
|
|
2738
2759
|
val packetInfo = K900ProtocolUtils.extractFilePacket(completePacket)
|
|
2739
2760
|
if (packetInfo != null && packetInfo.isValid) {
|
|
2740
|
-
|
|
2741
|
-
"LIVE: 📦 ✅ Packet validated: index=" +
|
|
2742
|
-
packetInfo.packIndex +
|
|
2743
|
-
", fileName=" +
|
|
2744
|
-
packetInfo.fileName
|
|
2745
|
-
)
|
|
2746
|
-
// Post to handler to process outside the lock
|
|
2747
|
-
val finalPacketInfo = packetInfo
|
|
2748
|
-
handler.post { processFilePacket(finalPacketInfo) }
|
|
2761
|
+
enqueueFilePacket(packetInfo)
|
|
2749
2762
|
} else {
|
|
2750
2763
|
Log.e(TAG, "Failed to extract/validate reassembled file packet")
|
|
2751
2764
|
}
|
|
@@ -2758,13 +2771,6 @@ class MentraLive : SGCManager() {
|
|
|
2758
2771
|
val remaining = filePacketBufferSize - pos
|
|
2759
2772
|
System.arraycopy(filePacketBuffer, pos, filePacketBuffer, 0, remaining)
|
|
2760
2773
|
filePacketBufferSize = remaining
|
|
2761
|
-
Bridge.log(
|
|
2762
|
-
"LIVE: 📦 Removed " +
|
|
2763
|
-
pos +
|
|
2764
|
-
" bytes, " +
|
|
2765
|
-
remaining +
|
|
2766
|
-
" bytes remaining in buffer"
|
|
2767
|
-
)
|
|
2768
2774
|
} else if (pos >= filePacketBufferSize) {
|
|
2769
2775
|
filePacketBufferSize = 0
|
|
2770
2776
|
}
|
|
@@ -2780,6 +2786,49 @@ class MentraLive : SGCManager() {
|
|
|
2780
2786
|
synchronized(filePacketBufferLock) { filePacketBufferSize = 0 }
|
|
2781
2787
|
}
|
|
2782
2788
|
|
|
2789
|
+
/**
|
|
2790
|
+
* Open the L2CAP CoC fast path for incoming file transfers (PSM 0x00C9).
|
|
2791
|
+
*
|
|
2792
|
+
* New BES2700 firmware registers an LE L2CAP CoC server; when the phone opens the channel,
|
|
2793
|
+
* the glasses send K900 file packets over it instead of GATT FILE_READ notifications.
|
|
2794
|
+
* Complete frames from the channel are fed into processFilePacketData — the exact same entry
|
|
2795
|
+
* point used by GATT FILE_READ notifications — so downstream reassembly (FileTransferSession,
|
|
2796
|
+
* transfer_complete) is unchanged. On any failure to open we stay on GATT notifications.
|
|
2797
|
+
*/
|
|
2798
|
+
private fun openL2capFileChannel() {
|
|
2799
|
+
if (!enableL2capFilePath) {
|
|
2800
|
+
return
|
|
2801
|
+
}
|
|
2802
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
2803
|
+
// createInsecureL2capChannel requires API 29
|
|
2804
|
+
Bridge.log("LIVE: L2CAP: unavailable, staying on GATT (requires Android 10+)")
|
|
2805
|
+
return
|
|
2806
|
+
}
|
|
2807
|
+
if (l2capFileChannel != null) {
|
|
2808
|
+
// Already open (or connecting) for this connection — e.g. repeated glasses_ready
|
|
2809
|
+
return
|
|
2810
|
+
}
|
|
2811
|
+
val device = connectedDevice
|
|
2812
|
+
if (device == null) {
|
|
2813
|
+
Log.w(TAG, "L2CAP: no connected device, staying on GATT")
|
|
2814
|
+
return
|
|
2815
|
+
}
|
|
2816
|
+
val channel =
|
|
2817
|
+
MentraLiveL2capChannel(L2CAP_FILE_PSM) { frame ->
|
|
2818
|
+
// Same code path as a GATT FILE_READ notification carrying this frame
|
|
2819
|
+
processFilePacketData(frame)
|
|
2820
|
+
}
|
|
2821
|
+
l2capFileChannel = channel
|
|
2822
|
+
channel.open(device)
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
/** Close the L2CAP file channel if open (call on disconnect). */
|
|
2826
|
+
private fun closeL2capFileChannel() {
|
|
2827
|
+
val channel = l2capFileChannel ?: return
|
|
2828
|
+
l2capFileChannel = null
|
|
2829
|
+
channel.close()
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2783
2832
|
/** Process data received from the glasses */
|
|
2784
2833
|
private fun processReceivedData(data: ByteArray?, size: Int) {
|
|
2785
2834
|
// Bridge.log("LIVE: Processing received data: " + bytesToHex(data));
|
|
@@ -2838,7 +2887,7 @@ class MentraLive : SGCManager() {
|
|
|
2838
2887
|
// structure
|
|
2839
2888
|
val packetInfo = K900ProtocolUtils.extractFilePacket(data)
|
|
2840
2889
|
if (packetInfo != null && packetInfo.isValid) {
|
|
2841
|
-
|
|
2890
|
+
enqueueFilePacket(packetInfo)
|
|
2842
2891
|
} else {
|
|
2843
2892
|
Log.e(TAG, "Thread-" + threadId + ": Failed to extract or validate file packet")
|
|
2844
2893
|
// BES chip handles ACKs automatically
|
|
@@ -2847,10 +2896,26 @@ class MentraLive : SGCManager() {
|
|
|
2847
2896
|
return // Exit after processing file packet
|
|
2848
2897
|
}
|
|
2849
2898
|
|
|
2899
|
+
if (cmdType == K900ProtocolUtils.CMD_TYPE_BINARY_MSG) {
|
|
2900
|
+
processBinaryWireFrame(data)
|
|
2901
|
+
return
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// Learn the glasses' K900 length endianness from the frame we just received so future
|
|
2905
|
+
// outbound frames match. Guards against a peer that never advertises wire_caps.
|
|
2906
|
+
K900LengthCodec.detectLength(data)?.let { detected ->
|
|
2907
|
+
peerK900Le = detected.endian == K900LengthCodec.Endian.LE
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2850
2910
|
// Otherwise it's a normal JSON message
|
|
2851
2911
|
val json = K900ProtocolUtils.processReceivedBytesToJson(data)
|
|
2852
2912
|
if (json != null) {
|
|
2853
|
-
|
|
2913
|
+
val expanded = expandCompactWireJson(json)
|
|
2914
|
+
if (expanded == null) {
|
|
2915
|
+
Log.w(TAG, "Thread-$threadId: Rejected unsupported compact wire form")
|
|
2916
|
+
return
|
|
2917
|
+
}
|
|
2918
|
+
processJsonMessage(expanded)
|
|
2854
2919
|
} else {
|
|
2855
2920
|
Log.w(TAG, "Thread-" + threadId + ": Failed to parse K900 protocol data")
|
|
2856
2921
|
// #region agent log [810da2] Hypothesis A+B: log header-declared length vs actual
|
|
@@ -3035,10 +3100,12 @@ class MentraLive : SGCManager() {
|
|
|
3035
3100
|
)
|
|
3036
3101
|
"speaking_status" -> handleSpeakingStatus(json.optBoolean("speaking", false))
|
|
3037
3102
|
"battery_status" -> {
|
|
3038
|
-
//
|
|
3103
|
+
// Percent only. battery_status derives from hm_batv, which carries no charge
|
|
3104
|
+
// bit — older glasses fabricate `charging` from a voltage threshold (>3.9V)
|
|
3105
|
+
// that reads "charging" for most of a discharging pack's range. Charging
|
|
3106
|
+
// state comes exclusively from the PMU charg bit in the sr_hrt heartbeat.
|
|
3039
3107
|
val percent = json.optInt("percent", batteryLevel)
|
|
3040
|
-
|
|
3041
|
-
updateBatteryStatus(percent, charging)
|
|
3108
|
+
updateBatteryStatus(percent, isCharging)
|
|
3042
3109
|
}
|
|
3043
3110
|
"pong" ->
|
|
3044
3111
|
// Process heartbeat pong response
|
|
@@ -3057,6 +3124,18 @@ class MentraLive : SGCManager() {
|
|
|
3057
3124
|
val ssid = json.optString("ssid", "")
|
|
3058
3125
|
val localIp = json.optString("local_ip", "")
|
|
3059
3126
|
|
|
3127
|
+
// Provisioning failure reason (e.g. connect_timeout). Sticky: routine
|
|
3128
|
+
// error-less status updates (link-state debounce, request_wifi_status)
|
|
3129
|
+
// must not clear a failure nothing recovered from — only a newer error
|
|
3130
|
+
// or a successful connection overwrites it.
|
|
3131
|
+
val wifiError = json.optString("error", "")
|
|
3132
|
+
if (wifiError.isNotEmpty()) {
|
|
3133
|
+
Bridge.log("LIVE: 🌐 WiFi provisioning error from glasses: $wifiError")
|
|
3134
|
+
DeviceStore.apply("glasses", "wifiError", wifiError)
|
|
3135
|
+
} else if (wifiConnectedStatus) {
|
|
3136
|
+
DeviceStore.apply("glasses", "wifiError", "")
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3060
3139
|
updateWifiStatus(wifiConnectedStatus, ssid, localIp)
|
|
3061
3140
|
}
|
|
3062
3141
|
"hotspot_status_update" -> {
|
|
@@ -3413,6 +3492,9 @@ class MentraLive : SGCManager() {
|
|
|
3413
3492
|
// Glasses SOC has booted and is ready for communication
|
|
3414
3493
|
Bridge.log("LIVE: 🎉 Received glasses_ready message - SOC is booted and ready!")
|
|
3415
3494
|
|
|
3495
|
+
// Negotiate wire capabilities advertised in the glasses_ready handshake.
|
|
3496
|
+
parsePeerWireCaps(json)
|
|
3497
|
+
|
|
3416
3498
|
// Set the ready flag to stop any future readiness checks
|
|
3417
3499
|
glassesReady = true
|
|
3418
3500
|
glassesReadyReceived = true
|
|
@@ -3422,12 +3504,18 @@ class MentraLive : SGCManager() {
|
|
|
3422
3504
|
|
|
3423
3505
|
// Stop the readiness check loop since we got confirmation
|
|
3424
3506
|
stopReadinessCheckLoop()
|
|
3507
|
+
advertiseFilePayloadCapabilityToBes()
|
|
3508
|
+
|
|
3509
|
+
// Try to open the L2CAP CoC fast path for file transfers. No-op when the
|
|
3510
|
+
// firmware doesn't support it — GATT notifications remain the default path.
|
|
3511
|
+
try {
|
|
3512
|
+
openL2capFileChannel()
|
|
3513
|
+
} catch (t: Throwable) {
|
|
3514
|
+
Bridge.log("LIVE: ⚠️ glasses_ready: openL2capFileChannel threw: " + t)
|
|
3515
|
+
}
|
|
3425
3516
|
|
|
3426
3517
|
// Send BLE MTU config to glasses so they can adjust file packet sizes.
|
|
3427
|
-
|
|
3428
|
-
// BES2700 chip often ignores higher negotiated MTUs and truncates to 253 bytes,
|
|
3429
|
-
// but we should respect the actual negotiated value if it's lower.
|
|
3430
|
-
val BES2700_MTU_LIMIT = 256 // BES2700's known notification size limit
|
|
3518
|
+
// Use the minimum of negotiated MTU and BES2700's datapath limit (509).
|
|
3431
3519
|
val effectiveMtu = Math.min(currentMtu, BES2700_MTU_LIMIT)
|
|
3432
3520
|
Bridge.log(
|
|
3433
3521
|
"LIVE: 📦 Sending BLE MTU config: negotiated=" +
|
|
@@ -3607,6 +3695,8 @@ class MentraLive : SGCManager() {
|
|
|
3607
3695
|
buildNumberInt = 0
|
|
3608
3696
|
Log.e(TAG, "Failed to parse build number as integer: " + buildNumberLegacy)
|
|
3609
3697
|
}
|
|
3698
|
+
parsePeerWireCaps(json)
|
|
3699
|
+
maybeSendWireHandshake()
|
|
3610
3700
|
}
|
|
3611
3701
|
"ota_download_progress" -> {
|
|
3612
3702
|
// Process OTA download progress from ASG client
|
|
@@ -3792,8 +3882,12 @@ class MentraLive : SGCManager() {
|
|
|
3792
3882
|
// Parse build number as integer for version checks
|
|
3793
3883
|
try {
|
|
3794
3884
|
val buildNumInt = Integer.parseInt(buildNum)
|
|
3885
|
+
buildNumberInt = buildNumInt
|
|
3795
3886
|
Bridge.log("LIVE: Parsed build number as integer: " + buildNumInt)
|
|
3887
|
+
parsePeerWireCaps(json)
|
|
3888
|
+
maybeSendWireHandshake()
|
|
3796
3889
|
} catch (e: NumberFormatException) {
|
|
3890
|
+
buildNumberInt = 0
|
|
3797
3891
|
Log.e(TAG, "Failed to parse build number as integer: " + buildNum)
|
|
3798
3892
|
}
|
|
3799
3893
|
}
|
|
@@ -4253,11 +4347,10 @@ class MentraLive : SGCManager() {
|
|
|
4253
4347
|
"%"
|
|
4254
4348
|
)
|
|
4255
4349
|
|
|
4256
|
-
//
|
|
4257
|
-
//
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
// Update battery status using the existing method
|
|
4350
|
+
// Percent only. sr_batv carries just voltage+percent; inferring
|
|
4351
|
+
// charging from voltage (>4.0V) reads "not charging" for most of a
|
|
4352
|
+
// genuinely-charging pack's range. Charging state comes exclusively
|
|
4353
|
+
// from the PMU charg bit in the sr_hrt heartbeat.
|
|
4261
4354
|
updateBatteryStatus(batteryPercentage, isCharging)
|
|
4262
4355
|
}
|
|
4263
4356
|
} catch (e: Exception) {
|
|
@@ -4561,6 +4654,11 @@ class MentraLive : SGCManager() {
|
|
|
4561
4654
|
* pattern
|
|
4562
4655
|
*/
|
|
4563
4656
|
private fun updateBatteryStatus(level: Int, isCharging: Boolean) {
|
|
4657
|
+
// Keep the field in sync: percent-only messages (battery_status/sr_batv) re-pass
|
|
4658
|
+
// it as the last-known charging state, so a stale field would clobber the value
|
|
4659
|
+
// the sr_hrt PMU charg bit established.
|
|
4660
|
+
this.isCharging = isCharging
|
|
4661
|
+
|
|
4564
4662
|
// Update parent SGCManager fields
|
|
4565
4663
|
DeviceStore.apply("glasses", "batteryLevel", level)
|
|
4566
4664
|
DeviceStore.apply("glasses", "charging", isCharging)
|
|
@@ -5279,6 +5377,7 @@ class MentraLive : SGCManager() {
|
|
|
5279
5377
|
override fun requestPhoto(request: PhotoRequest) {
|
|
5280
5378
|
val requestId = request.requestId
|
|
5281
5379
|
val size = request.size.value
|
|
5380
|
+
val mode = request.mode.value
|
|
5282
5381
|
val webhookUrl = request.webhookUrl
|
|
5283
5382
|
val authToken = request.authToken
|
|
5284
5383
|
val compress = request.compress.value
|
|
@@ -5292,6 +5391,8 @@ class MentraLive : SGCManager() {
|
|
|
5292
5391
|
requestId +
|
|
5293
5392
|
" with size: " +
|
|
5294
5393
|
size +
|
|
5394
|
+
", mode=" +
|
|
5395
|
+
mode +
|
|
5295
5396
|
", webhookUrl: " +
|
|
5296
5397
|
webhookUrl +
|
|
5297
5398
|
", authToken: " +
|
|
@@ -5329,6 +5430,7 @@ class MentraLive : SGCManager() {
|
|
|
5329
5430
|
if (size != null && !size.isEmpty()) {
|
|
5330
5431
|
json.put("size", size)
|
|
5331
5432
|
}
|
|
5433
|
+
json.put("mode", mode)
|
|
5332
5434
|
if (compress != null && !compress.isEmpty()) {
|
|
5333
5435
|
json.put("compress", compress)
|
|
5334
5436
|
} else {
|
|
@@ -5356,7 +5458,7 @@ class MentraLive : SGCManager() {
|
|
|
5356
5458
|
val bleImgId = "I" + String.format("%09d", System.currentTimeMillis() % 1000000000)
|
|
5357
5459
|
json.put("bleImgId", bleImgId)
|
|
5358
5460
|
|
|
5359
|
-
// Use auto mode by default - glasses
|
|
5461
|
+
// Use auto mode by default - glasses try WiFi direct upload, BLE fallback via bleImgId
|
|
5360
5462
|
json.put("transferMethod", "auto")
|
|
5361
5463
|
|
|
5362
5464
|
// Always prepare for potential BLE transfer
|
|
@@ -5369,7 +5471,9 @@ class MentraLive : SGCManager() {
|
|
|
5369
5471
|
|
|
5370
5472
|
Bridge.log("LIVE: Using auto transfer mode with BLE fallback ID: " + bleImgId)
|
|
5371
5473
|
Bridge.log(
|
|
5372
|
-
"LIVE: PHOTO PIPELINE [5b/6] JSON ready
|
|
5474
|
+
"LIVE: PHOTO PIPELINE [5b/6] JSON ready mode=" +
|
|
5475
|
+
mode +
|
|
5476
|
+
" — " +
|
|
5373
5477
|
summarizeOutgoingMessage(json.toString()) +
|
|
5374
5478
|
", wakeup=true"
|
|
5375
5479
|
)
|
|
@@ -6026,6 +6130,7 @@ class MentraLive : SGCManager() {
|
|
|
6026
6130
|
|
|
6027
6131
|
// Cancel any pending handlers
|
|
6028
6132
|
handler.removeCallbacksAndMessages(null)
|
|
6133
|
+
fileProcessingHandler.removeCallbacksAndMessages(null)
|
|
6029
6134
|
heartbeatHandler.removeCallbacksAndMessages(null)
|
|
6030
6135
|
rssiReadHandler.removeCallbacksAndMessages(null)
|
|
6031
6136
|
micBeatHandler.removeCallbacksAndMessages(null)
|
|
@@ -6043,6 +6148,9 @@ class MentraLive : SGCManager() {
|
|
|
6043
6148
|
// sendRgbLedControlAuthority(false);
|
|
6044
6149
|
// }
|
|
6045
6150
|
|
|
6151
|
+
// Close the L2CAP file channel (if the fast path was open)
|
|
6152
|
+
closeL2capFileChannel()
|
|
6153
|
+
|
|
6046
6154
|
// Disconnect from GATT if connected
|
|
6047
6155
|
closeGattQuietly(true)
|
|
6048
6156
|
|
|
@@ -6054,6 +6162,7 @@ class MentraLive : SGCManager() {
|
|
|
6054
6162
|
|
|
6055
6163
|
// Clear file packet reassembly buffer
|
|
6056
6164
|
clearFilePacketBuffer()
|
|
6165
|
+
fileProcessingThread.quitSafely()
|
|
6057
6166
|
|
|
6058
6167
|
// Reset state variables
|
|
6059
6168
|
reconnectAttempts = 0
|
|
@@ -6366,7 +6475,8 @@ class MentraLive : SGCManager() {
|
|
|
6366
6475
|
val packedData =
|
|
6367
6476
|
K900ProtocolUtils.packDataToK900(
|
|
6368
6477
|
jsonStr.toByteArray(StandardCharsets.UTF_8),
|
|
6369
|
-
K900ProtocolUtils.CMD_TYPE_STRING
|
|
6478
|
+
K900ProtocolUtils.CMD_TYPE_STRING,
|
|
6479
|
+
k900LengthEndian()
|
|
6370
6480
|
)
|
|
6371
6481
|
|
|
6372
6482
|
queueData(packedData)
|
|
@@ -6550,7 +6660,8 @@ class MentraLive : SGCManager() {
|
|
|
6550
6660
|
val packedData =
|
|
6551
6661
|
K900ProtocolUtils.packDataToK900(
|
|
6552
6662
|
jsonStr.toByteArray(StandardCharsets.UTF_8),
|
|
6553
|
-
K900ProtocolUtils.CMD_TYPE_STRING
|
|
6663
|
+
K900ProtocolUtils.CMD_TYPE_STRING,
|
|
6664
|
+
k900LengthEndian()
|
|
6554
6665
|
)
|
|
6555
6666
|
queueData(packedData)
|
|
6556
6667
|
} catch (e: JSONException) {
|
|
@@ -6579,7 +6690,8 @@ class MentraLive : SGCManager() {
|
|
|
6579
6690
|
val packedData =
|
|
6580
6691
|
K900ProtocolUtils.packDataToK900(
|
|
6581
6692
|
jsonStr.toByteArray(StandardCharsets.UTF_8),
|
|
6582
|
-
K900ProtocolUtils.CMD_TYPE_STRING
|
|
6693
|
+
K900ProtocolUtils.CMD_TYPE_STRING,
|
|
6694
|
+
k900LengthEndian()
|
|
6583
6695
|
)
|
|
6584
6696
|
queueData(packedData)
|
|
6585
6697
|
} catch (e: JSONException) {
|
|
@@ -6653,7 +6765,8 @@ class MentraLive : SGCManager() {
|
|
|
6653
6765
|
val packedData =
|
|
6654
6766
|
K900ProtocolUtils.packDataToK900(
|
|
6655
6767
|
jsonStr.toByteArray(StandardCharsets.UTF_8),
|
|
6656
|
-
K900ProtocolUtils.CMD_TYPE_STRING
|
|
6768
|
+
K900ProtocolUtils.CMD_TYPE_STRING,
|
|
6769
|
+
k900LengthEndian()
|
|
6657
6770
|
)
|
|
6658
6771
|
Bridge.log("LIVE: AUDIO: Sending cs_getvol command: " + jsonStr)
|
|
6659
6772
|
queueData(packedData)
|
|
@@ -6677,7 +6790,8 @@ class MentraLive : SGCManager() {
|
|
|
6677
6790
|
val packedData =
|
|
6678
6791
|
K900ProtocolUtils.packDataToK900(
|
|
6679
6792
|
jsonStr.toByteArray(StandardCharsets.UTF_8),
|
|
6680
|
-
K900ProtocolUtils.CMD_TYPE_STRING
|
|
6793
|
+
K900ProtocolUtils.CMD_TYPE_STRING,
|
|
6794
|
+
k900LengthEndian()
|
|
6681
6795
|
)
|
|
6682
6796
|
queueData(packedData)
|
|
6683
6797
|
return true
|
|
@@ -7039,6 +7153,194 @@ class MentraLive : SGCManager() {
|
|
|
7039
7153
|
}
|
|
7040
7154
|
}
|
|
7041
7155
|
|
|
7156
|
+
/** Negotiated K900 STRING length endianness for outbound frames to the glasses. */
|
|
7157
|
+
private fun k900LengthEndian(): K900LengthCodec.Endian =
|
|
7158
|
+
if (peerK900Le) K900LengthCodec.Endian.LE else K900LengthCodec.Endian.BE
|
|
7159
|
+
|
|
7160
|
+
/**
|
|
7161
|
+
* Parse a {@code wire_caps} object from a version_info/glasses_ready message and update the
|
|
7162
|
+
* negotiated per-link endianness and binary support. Missing wire_caps leaves the legacy
|
|
7163
|
+
* defaults (BE, no binary) untouched so older glasses keep working.
|
|
7164
|
+
*/
|
|
7165
|
+
private fun parsePeerWireCaps(json: JSONObject) {
|
|
7166
|
+
val caps = json.optJSONObject("wire_caps") ?: return
|
|
7167
|
+
if (caps.optBoolean("k900_le", false)) {
|
|
7168
|
+
if (!peerK900Le) {
|
|
7169
|
+
peerK900Le = true
|
|
7170
|
+
Bridge.log("LIVE: wire_caps negotiated k900 endian=LE")
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
if (caps.has("binary")) {
|
|
7174
|
+
peerWireCapsBinary = caps.optBoolean("binary", false)
|
|
7175
|
+
}
|
|
7176
|
+
if (caps.has("file_payload_v2")) {
|
|
7177
|
+
peerFilePayloadV2 = caps.optBoolean("file_payload_v2", false)
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
7180
|
+
|
|
7181
|
+
private fun advertiseFilePayloadCapabilityToBes() {
|
|
7182
|
+
if (!peerFilePayloadV2) return
|
|
7183
|
+
try {
|
|
7184
|
+
val command = JSONObject()
|
|
7185
|
+
command.put("C", "cs_file_payload")
|
|
7186
|
+
command.put("B", "{\"max\":" + K900ProtocolUtils.FILE_PACK_SIZE + "}")
|
|
7187
|
+
val packed =
|
|
7188
|
+
K900ProtocolUtils.packDataToK900(
|
|
7189
|
+
command.toString().toByteArray(StandardCharsets.UTF_8),
|
|
7190
|
+
K900ProtocolUtils.CMD_TYPE_STRING,
|
|
7191
|
+
k900LengthEndian()
|
|
7192
|
+
)
|
|
7193
|
+
queueData(packed)
|
|
7194
|
+
Bridge.log(
|
|
7195
|
+
"LIVE: 📦 Advertised file payload ceiling " +
|
|
7196
|
+
K900ProtocolUtils.FILE_PACK_SIZE +
|
|
7197
|
+
" to BES"
|
|
7198
|
+
)
|
|
7199
|
+
} catch (e: JSONException) {
|
|
7200
|
+
Log.e(TAG, "Failed to advertise file payload capability", e)
|
|
7201
|
+
}
|
|
7202
|
+
}
|
|
7203
|
+
|
|
7204
|
+
private fun maybeSendWireHandshake() {
|
|
7205
|
+
// Only attempt the v2 binary handshake once the glasses have advertised binary support via
|
|
7206
|
+
// wire_caps. Older builds that report build>=5 but lack wire_caps stay on the legacy path.
|
|
7207
|
+
if (buildNumberInt < 5 ||
|
|
7208
|
+
!peerWireCapsBinary ||
|
|
7209
|
+
wireHandshakeQueued ||
|
|
7210
|
+
peerWireProtocolVersion >= BleWireProtocol.PROTOCOL_V2
|
|
7211
|
+
) {
|
|
7212
|
+
return
|
|
7213
|
+
}
|
|
7214
|
+
sendWireHandshake()
|
|
7215
|
+
}
|
|
7216
|
+
|
|
7217
|
+
private fun sendWireHandshake() {
|
|
7218
|
+
if (buildNumberInt < 5) {
|
|
7219
|
+
return
|
|
7220
|
+
}
|
|
7221
|
+
try {
|
|
7222
|
+
val payload = BleWireProtocol.HANDSHAKE_PAYLOAD_V2.toByteArray(StandardCharsets.UTF_8)
|
|
7223
|
+
var flags = BleWireProtocol.BLE_WIRE_FLAG_HANDSHAKE.toInt()
|
|
7224
|
+
flags = flags or BleWireProtocol.BLE_WIRE_FLAG_FIRST_FRAG.toInt()
|
|
7225
|
+
flags = flags or BleWireProtocol.BLE_WIRE_FLAG_LAST_FRAG.toInt()
|
|
7226
|
+
val packed =
|
|
7227
|
+
K900ProtocolUtils.packBinaryFragment(
|
|
7228
|
+
flags.toByte(),
|
|
7229
|
+
0,
|
|
7230
|
+
0,
|
|
7231
|
+
1,
|
|
7232
|
+
payload
|
|
7233
|
+
)
|
|
7234
|
+
Bridge.log("LIVE: Sending BLE wire v2 handshake")
|
|
7235
|
+
wireHandshakeQueued = true
|
|
7236
|
+
queueData(packed, null)
|
|
7237
|
+
} catch (e: Exception) {
|
|
7238
|
+
Log.e(TAG, "Failed to send wire handshake", e)
|
|
7239
|
+
}
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7242
|
+
private fun activateBinaryWireV2Session(logMessage: String) {
|
|
7243
|
+
peerWireProtocolVersion = BleWireProtocol.PROTOCOL_V2
|
|
7244
|
+
useBinaryWireProtocol = true
|
|
7245
|
+
wireHandshakeQueued = false
|
|
7246
|
+
peerK900Le = true
|
|
7247
|
+
BleJsonCompact.markSessionConnected(System.currentTimeMillis())
|
|
7248
|
+
Bridge.log(logMessage)
|
|
7249
|
+
}
|
|
7250
|
+
|
|
7251
|
+
private fun handlePeerWireHandshake() {
|
|
7252
|
+
activateBinaryWireV2Session("LIVE: Peer confirmed BLE wire protocol v2")
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7255
|
+
private fun processBinaryWireFrame(data: ByteArray) {
|
|
7256
|
+
val info = K900ProtocolUtils.extractBinaryFragmentInfo(data) ?: run {
|
|
7257
|
+
Log.w(TAG, "Failed to parse binary wire frame")
|
|
7258
|
+
return
|
|
7259
|
+
}
|
|
7260
|
+
|
|
7261
|
+
if (BleWireProtocol.isHandshakeV2(info)) {
|
|
7262
|
+
handlePeerWireHandshake()
|
|
7263
|
+
return
|
|
7264
|
+
}
|
|
7265
|
+
|
|
7266
|
+
if (!useBinaryWireProtocol && buildNumberInt >= 5) {
|
|
7267
|
+
activateBinaryWireV2Session(
|
|
7268
|
+
"LIVE: Auto-enabled BLE wire v2 from incoming binary frame"
|
|
7269
|
+
)
|
|
7270
|
+
}
|
|
7271
|
+
|
|
7272
|
+
val reassembled =
|
|
7273
|
+
incomingChunkReassembler.addBinaryFragment(
|
|
7274
|
+
info.msgId,
|
|
7275
|
+
info.fragIdx,
|
|
7276
|
+
info.fragCount,
|
|
7277
|
+
info.payload
|
|
7278
|
+
)
|
|
7279
|
+
?: return
|
|
7280
|
+
|
|
7281
|
+
try {
|
|
7282
|
+
val jsonStr = String(reassembled, StandardCharsets.UTF_8)
|
|
7283
|
+
val json = expandCompactWireJson(JSONObject(jsonStr))
|
|
7284
|
+
if (json == null) {
|
|
7285
|
+
Log.w(TAG, "Rejected unsupported compact reassembled wire form")
|
|
7286
|
+
return
|
|
7287
|
+
}
|
|
7288
|
+
logWireMetrics(
|
|
7289
|
+
reassembled.size,
|
|
7290
|
+
data.size,
|
|
7291
|
+
info.fragCount,
|
|
7292
|
+
BleWireProtocol.PROTOCOL_V2,
|
|
7293
|
+
"glasses_to_phone"
|
|
7294
|
+
)
|
|
7295
|
+
processJsonMessage(json)
|
|
7296
|
+
} catch (e: JSONException) {
|
|
7297
|
+
Log.e(TAG, "Failed to parse reassembled binary wire JSON", e)
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7300
|
+
|
|
7301
|
+
private fun compactWireJson(json: JSONObject): JSONObject {
|
|
7302
|
+
if (!useBinaryWireProtocol || buildNumberInt < 5) {
|
|
7303
|
+
return json
|
|
7304
|
+
}
|
|
7305
|
+
return try {
|
|
7306
|
+
BleJsonCompact.encode(json)
|
|
7307
|
+
} catch (_: JSONException) {
|
|
7308
|
+
json
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
|
|
7312
|
+
private fun expandCompactWireJson(json: JSONObject): JSONObject? {
|
|
7313
|
+
if (!useBinaryWireProtocol || buildNumberInt < 5) {
|
|
7314
|
+
return json
|
|
7315
|
+
}
|
|
7316
|
+
return try {
|
|
7317
|
+
BleJsonCompact.decodeIfSupported(json)
|
|
7318
|
+
} catch (_: JSONException) {
|
|
7319
|
+
json
|
|
7320
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
|
|
7323
|
+
private fun logWireMetrics(
|
|
7324
|
+
payloadBytes: Int,
|
|
7325
|
+
wireBytes: Int,
|
|
7326
|
+
packetCount: Int,
|
|
7327
|
+
protocolVersion: Int,
|
|
7328
|
+
direction: String
|
|
7329
|
+
) {
|
|
7330
|
+
Bridge.log(
|
|
7331
|
+
"BLE_TRACE direction=" +
|
|
7332
|
+
direction +
|
|
7333
|
+
" proto=v" +
|
|
7334
|
+
protocolVersion +
|
|
7335
|
+
" payload=" +
|
|
7336
|
+
payloadBytes +
|
|
7337
|
+
" wire=" +
|
|
7338
|
+
wireBytes +
|
|
7339
|
+
" packets=" +
|
|
7340
|
+
packetCount
|
|
7341
|
+
)
|
|
7342
|
+
}
|
|
7343
|
+
|
|
7042
7344
|
/**
|
|
7043
7345
|
* Send data directly to the glasses using the K900 protocol utility. This method uses
|
|
7044
7346
|
* K900ProtocolUtils.packJsonToK900 to handle C-wrapping and protocol formatting. Large messages
|
|
@@ -7052,8 +7354,19 @@ class MentraLive : SGCManager() {
|
|
|
7052
7354
|
return
|
|
7053
7355
|
}
|
|
7054
7356
|
|
|
7357
|
+
val wireData =
|
|
7358
|
+
try {
|
|
7359
|
+
if (useBinaryWireProtocol && buildNumberInt >= 5) {
|
|
7360
|
+
compactWireJson(JSONObject(data)).toString()
|
|
7361
|
+
} else {
|
|
7362
|
+
data
|
|
7363
|
+
}
|
|
7364
|
+
} catch (_: JSONException) {
|
|
7365
|
+
data
|
|
7366
|
+
}
|
|
7367
|
+
|
|
7055
7368
|
try {
|
|
7056
|
-
val outgoingSummary = summarizeOutgoingMessage(
|
|
7369
|
+
val outgoingSummary = summarizeOutgoingMessage(wireData)
|
|
7057
7370
|
val commandTraceInfo = parseOutgoingBleCommandTraceInfo(data)
|
|
7058
7371
|
val isPhotoRequest = outgoingSummary.contains("type=take_photo")
|
|
7059
7372
|
if (isPhotoRequest) {
|
|
@@ -7065,10 +7378,15 @@ class MentraLive : SGCManager() {
|
|
|
7065
7378
|
)
|
|
7066
7379
|
}
|
|
7067
7380
|
|
|
7381
|
+
if (useBinaryWireProtocol && buildNumberInt >= 5) {
|
|
7382
|
+
sendDataToGlassesBinary(wireData, wakeup, commandTraceInfo, isPhotoRequest)
|
|
7383
|
+
return
|
|
7384
|
+
}
|
|
7385
|
+
|
|
7068
7386
|
// First check if the message needs chunking
|
|
7069
7387
|
// Create a test C-wrapped version to check size
|
|
7070
7388
|
val testWrapper = JSONObject()
|
|
7071
|
-
testWrapper.put("C",
|
|
7389
|
+
testWrapper.put("C", wireData)
|
|
7072
7390
|
if (wakeup) {
|
|
7073
7391
|
testWrapper.put("W", 1)
|
|
7074
7392
|
}
|
|
@@ -7086,14 +7404,14 @@ class MentraLive : SGCManager() {
|
|
|
7086
7404
|
// Extract message ID if present for ACK tracking
|
|
7087
7405
|
var messageId = -1L
|
|
7088
7406
|
try {
|
|
7089
|
-
val originalJson = JSONObject(
|
|
7407
|
+
val originalJson = JSONObject(wireData)
|
|
7090
7408
|
messageId = originalJson.optLong("mId", -1)
|
|
7091
7409
|
} catch (e: JSONException) {
|
|
7092
7410
|
// Not a JSON message or no mId, that's okay
|
|
7093
7411
|
}
|
|
7094
7412
|
|
|
7095
7413
|
// Create chunks
|
|
7096
|
-
val chunks = MessageChunker.createChunks(
|
|
7414
|
+
val chunks = MessageChunker.createChunks(wireData, messageId, wakeup)
|
|
7097
7415
|
Bridge.log("LIVE: Sending " + chunks.size + " chunks")
|
|
7098
7416
|
if (isPhotoRequest) {
|
|
7099
7417
|
Bridge.log(
|
|
@@ -7108,11 +7426,12 @@ class MentraLive : SGCManager() {
|
|
|
7108
7426
|
val chunk = chunks[i]
|
|
7109
7427
|
val chunkStr = chunk.toString()
|
|
7110
7428
|
|
|
7111
|
-
// Pack each chunk using the normal K900 protocol
|
|
7429
|
+
// Pack each chunk using the normal K900 protocol with the negotiated endianness
|
|
7112
7430
|
val packedData =
|
|
7113
7431
|
K900ProtocolUtils.packJsonToK900(
|
|
7114
7432
|
chunkStr,
|
|
7115
|
-
wakeup && i == 0
|
|
7433
|
+
wakeup && i == 0,
|
|
7434
|
+
k900LengthEndian()
|
|
7116
7435
|
) // Only wakeup on first chunk
|
|
7117
7436
|
|
|
7118
7437
|
val trace =
|
|
@@ -7134,7 +7453,7 @@ class MentraLive : SGCManager() {
|
|
|
7134
7453
|
mapOf(
|
|
7135
7454
|
"chunkJsonBytes" to
|
|
7136
7455
|
chunkStr.toByteArray(StandardCharsets.UTF_8).size,
|
|
7137
|
-
"messageBytes" to
|
|
7456
|
+
"messageBytes" to wireData.toByteArray(StandardCharsets.UTF_8).size
|
|
7138
7457
|
)
|
|
7139
7458
|
)
|
|
7140
7459
|
|
|
@@ -7157,17 +7476,18 @@ class MentraLive : SGCManager() {
|
|
|
7157
7476
|
}
|
|
7158
7477
|
} else {
|
|
7159
7478
|
// Normal single message transmission
|
|
7160
|
-
Bridge.log("LIVE: Sending data to glasses: " +
|
|
7479
|
+
Bridge.log("LIVE: Sending data to glasses: " + wireData)
|
|
7161
7480
|
|
|
7162
|
-
// Pack the data using the centralized utility
|
|
7163
|
-
val packedData =
|
|
7481
|
+
// Pack the data using the centralized utility with the negotiated endianness
|
|
7482
|
+
val packedData =
|
|
7483
|
+
K900ProtocolUtils.packJsonToK900(wireData, wakeup, k900LengthEndian())
|
|
7164
7484
|
val trace =
|
|
7165
7485
|
createBleWriteTrace(
|
|
7166
7486
|
commandTraceInfo,
|
|
7167
7487
|
null,
|
|
7168
7488
|
null,
|
|
7169
7489
|
null,
|
|
7170
|
-
|
|
7490
|
+
wireData.toByteArray(StandardCharsets.UTF_8).size,
|
|
7171
7491
|
packedData.size,
|
|
7172
7492
|
wakeup,
|
|
7173
7493
|
false
|
|
@@ -7175,7 +7495,7 @@ class MentraLive : SGCManager() {
|
|
|
7175
7495
|
logBleChunkTrace(
|
|
7176
7496
|
"created",
|
|
7177
7497
|
trace,
|
|
7178
|
-
mapOf("messageBytes" to
|
|
7498
|
+
mapOf("messageBytes" to wireData.toByteArray(StandardCharsets.UTF_8).size)
|
|
7179
7499
|
)
|
|
7180
7500
|
|
|
7181
7501
|
// Queue the data for sending
|
|
@@ -7193,6 +7513,91 @@ class MentraLive : SGCManager() {
|
|
|
7193
7513
|
}
|
|
7194
7514
|
}
|
|
7195
7515
|
|
|
7516
|
+
private fun sendDataToGlassesBinary(
|
|
7517
|
+
data: String,
|
|
7518
|
+
wakeup: Boolean,
|
|
7519
|
+
commandTraceInfo: OutgoingBleCommandTraceInfo,
|
|
7520
|
+
isPhotoRequest: Boolean
|
|
7521
|
+
) {
|
|
7522
|
+
val payloadBytes = data.toByteArray(StandardCharsets.UTF_8)
|
|
7523
|
+
var messageId = 0
|
|
7524
|
+
var ackRequested = false
|
|
7525
|
+
try {
|
|
7526
|
+
val originalJson = JSONObject(data)
|
|
7527
|
+
messageId = (originalJson.optLong("mId", 0L) and 0xFFFFL).toInt()
|
|
7528
|
+
ackRequested = originalJson.has("mId")
|
|
7529
|
+
} catch (_: JSONException) {
|
|
7530
|
+
// Raw non-JSON payloads are still sent as a single binary fragment.
|
|
7531
|
+
}
|
|
7532
|
+
|
|
7533
|
+
val fragments =
|
|
7534
|
+
MessageChunker.createBinaryFragments(
|
|
7535
|
+
payloadBytes,
|
|
7536
|
+
messageId,
|
|
7537
|
+
wakeup,
|
|
7538
|
+
ackRequested
|
|
7539
|
+
)
|
|
7540
|
+
var totalWireBytes = 0
|
|
7541
|
+
for (i in fragments.indices) {
|
|
7542
|
+
val fragment = fragments[i]
|
|
7543
|
+
val packedData =
|
|
7544
|
+
K900ProtocolUtils.packBinaryFragment(
|
|
7545
|
+
fragment.flags,
|
|
7546
|
+
fragment.msgId,
|
|
7547
|
+
fragment.fragIdx,
|
|
7548
|
+
fragment.fragCount,
|
|
7549
|
+
fragment.payload
|
|
7550
|
+
)
|
|
7551
|
+
totalWireBytes += packedData.size
|
|
7552
|
+
|
|
7553
|
+
val trace =
|
|
7554
|
+
createBleWriteTrace(
|
|
7555
|
+
commandTraceInfo,
|
|
7556
|
+
null,
|
|
7557
|
+
fragment.fragIdx,
|
|
7558
|
+
fragment.fragCount,
|
|
7559
|
+
fragment.payload.size,
|
|
7560
|
+
packedData.size,
|
|
7561
|
+
wakeup && i == 0,
|
|
7562
|
+
fragments.size > 1
|
|
7563
|
+
)
|
|
7564
|
+
logBleChunkTrace(
|
|
7565
|
+
"created",
|
|
7566
|
+
trace,
|
|
7567
|
+
mapOf(
|
|
7568
|
+
"messageBytes" to payloadBytes.size,
|
|
7569
|
+
"wireProtocol" to "v2"
|
|
7570
|
+
)
|
|
7571
|
+
)
|
|
7572
|
+
queueData(packedData, trace)
|
|
7573
|
+
|
|
7574
|
+
if (i < fragments.size - 1) {
|
|
7575
|
+
try {
|
|
7576
|
+
Thread.sleep(50)
|
|
7577
|
+
} catch (e: InterruptedException) {
|
|
7578
|
+
Log.w(TAG, "Interrupted during binary fragment delay")
|
|
7579
|
+
}
|
|
7580
|
+
}
|
|
7581
|
+
}
|
|
7582
|
+
|
|
7583
|
+
logWireMetrics(
|
|
7584
|
+
payloadBytes.size,
|
|
7585
|
+
totalWireBytes,
|
|
7586
|
+
fragments.size,
|
|
7587
|
+
BleWireProtocol.PROTOCOL_V2,
|
|
7588
|
+
"phone_to_glasses"
|
|
7589
|
+
)
|
|
7590
|
+
|
|
7591
|
+
if (isPhotoRequest) {
|
|
7592
|
+
Bridge.log(
|
|
7593
|
+
"LIVE: PHOTO PIPELINE BLE handoff — binary v2 queued " +
|
|
7594
|
+
fragments.size +
|
|
7595
|
+
" fragments, wireBytes=" +
|
|
7596
|
+
totalWireBytes
|
|
7597
|
+
)
|
|
7598
|
+
}
|
|
7599
|
+
}
|
|
7600
|
+
|
|
7196
7601
|
private fun parseOutgoingBleCommandTraceInfo(payload: String): OutgoingBleCommandTraceInfo {
|
|
7197
7602
|
return try {
|
|
7198
7603
|
val obj = JSONObject(payload)
|
|
@@ -7368,7 +7773,7 @@ class MentraLive : SGCManager() {
|
|
|
7368
7773
|
|
|
7369
7774
|
private fun summarizeOutgoingMessage(payload: String?): String {
|
|
7370
7775
|
if (payload == null || payload.isEmpty()) {
|
|
7371
|
-
return "type=unknown, requestId=none, appId=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, iso=none, mId=none"
|
|
7776
|
+
return "type=unknown, requestId=none, appId=none, mode=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, iso=none, mId=none"
|
|
7372
7777
|
}
|
|
7373
7778
|
try {
|
|
7374
7779
|
val obj = JSONObject(payload)
|
|
@@ -7377,6 +7782,7 @@ class MentraLive : SGCManager() {
|
|
|
7377
7782
|
val appId = obj.optString("appId", "none")
|
|
7378
7783
|
val transferMethod = obj.optString("transferMethod", "none")
|
|
7379
7784
|
val bleImgId = obj.optString("bleImgId", "none")
|
|
7785
|
+
val mode = if (obj.has("mode")) obj.optString("mode", "photo") else "none"
|
|
7380
7786
|
val exposure =
|
|
7381
7787
|
if (obj.has("exposureTimeNs")) obj.optLong("exposureTimeNs").toString()
|
|
7382
7788
|
else "none"
|
|
@@ -7392,6 +7798,8 @@ class MentraLive : SGCManager() {
|
|
|
7392
7798
|
transferMethod +
|
|
7393
7799
|
", bleImgId=" +
|
|
7394
7800
|
bleImgId +
|
|
7801
|
+
", mode=" +
|
|
7802
|
+
mode +
|
|
7395
7803
|
", exposureTimeNs=" +
|
|
7396
7804
|
exposure +
|
|
7397
7805
|
", iso=" +
|
|
@@ -7701,6 +8109,11 @@ class MentraLive : SGCManager() {
|
|
|
7701
8109
|
// File Transfer Methods
|
|
7702
8110
|
// ---------------------------------------
|
|
7703
8111
|
|
|
8112
|
+
/** Keep file assembly ordered without blocking BLE callbacks or the Android main looper. */
|
|
8113
|
+
private fun enqueueFilePacket(packetInfo: K900ProtocolUtils.FilePacketInfo) {
|
|
8114
|
+
fileProcessingHandler.post { processFilePacket(packetInfo) }
|
|
8115
|
+
}
|
|
8116
|
+
|
|
7704
8117
|
/** Process a received file packet */
|
|
7705
8118
|
private fun processFilePacket(packetInfo: K900ProtocolUtils.FilePacketInfo) {
|
|
7706
8119
|
// Calculate total packets based on actual pack size (not hardcoded FILE_PACK_SIZE)
|
|
@@ -7708,18 +8121,15 @@ class MentraLive : SGCManager() {
|
|
|
7708
8121
|
if (packetInfo.packSize > 0)
|
|
7709
8122
|
(packetInfo.fileSize + packetInfo.packSize - 1) / packetInfo.packSize
|
|
7710
8123
|
else 1
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
packetInfo.packSize +
|
|
7721
|
-
" bytes)"
|
|
7722
|
-
)
|
|
8124
|
+
if (packetInfo.packIndex % FILE_PACKET_LOG_INTERVAL == 0 ||
|
|
8125
|
+
packetInfo.packIndex == totalPackets - 1
|
|
8126
|
+
) {
|
|
8127
|
+
Log.d(
|
|
8128
|
+
TAG,
|
|
8129
|
+
"File transfer ${packetInfo.fileName}: " +
|
|
8130
|
+
"${packetInfo.packIndex + 1}/$totalPackets packets"
|
|
8131
|
+
)
|
|
8132
|
+
}
|
|
7723
8133
|
|
|
7724
8134
|
// Check if this is a BLE photo transfer we're tracking
|
|
7725
8135
|
// The filename might have an extension (.avif or .jpg), but we track by ID only
|
|
@@ -7729,8 +8139,6 @@ class MentraLive : SGCManager() {
|
|
|
7729
8139
|
bleImgId = bleImgId.substring(0, dotIndex)
|
|
7730
8140
|
}
|
|
7731
8141
|
|
|
7732
|
-
Bridge.log("LIVE: 📦 BLE photo transfer packet for requestId: " + bleImgId)
|
|
7733
|
-
|
|
7734
8142
|
val incidentRelay = bleIncidentLogRelays[bleImgId]
|
|
7735
8143
|
if (incidentRelay != null) {
|
|
7736
8144
|
Bridge.log("LIVE: 📦 BLE incident log relay packet for: " + bleImgId)
|
|
@@ -7784,18 +8192,8 @@ class MentraLive : SGCManager() {
|
|
|
7784
8192
|
}
|
|
7785
8193
|
|
|
7786
8194
|
val photoTransfer = blePhotoTransfers[bleImgId]
|
|
7787
|
-
Bridge.log(
|
|
7788
|
-
"LIVE: 📦 BLE photo transfer for requestId: " +
|
|
7789
|
-
bleImgId +
|
|
7790
|
-
" found: " +
|
|
7791
|
-
(photoTransfer != null)
|
|
7792
|
-
)
|
|
7793
8195
|
if (photoTransfer != null) {
|
|
7794
8196
|
// This is a BLE photo transfer
|
|
7795
|
-
Bridge.log(
|
|
7796
|
-
"LIVE: 📦 BLE photo transfer packet for requestId: " + photoTransfer.requestId
|
|
7797
|
-
)
|
|
7798
|
-
|
|
7799
8197
|
// Get or create session for this transfer
|
|
7800
8198
|
if (photoTransfer.session == null) {
|
|
7801
8199
|
photoTransfer.session =
|
|
@@ -7907,13 +8305,6 @@ class MentraLive : SGCManager() {
|
|
|
7907
8305
|
val added = session.addPacket(packetInfo.packIndex, packetInfo.data)
|
|
7908
8306
|
|
|
7909
8307
|
if (added) {
|
|
7910
|
-
// BES chip handles ACKs automatically
|
|
7911
|
-
Bridge.log(
|
|
7912
|
-
"LIVE: 📦 Packet " +
|
|
7913
|
-
packetInfo.packIndex +
|
|
7914
|
-
" received successfully (BES will auto-ACK)"
|
|
7915
|
-
)
|
|
7916
|
-
|
|
7917
8308
|
// Check completion when final packet arrives or transfer is complete
|
|
7918
8309
|
if (session.shouldCheckCompletion(packetInfo.packIndex)) {
|
|
7919
8310
|
if (session.isComplete) {
|
|
@@ -8400,7 +8791,8 @@ class MentraLive : SGCManager() {
|
|
|
8400
8791
|
val packedData =
|
|
8401
8792
|
K900ProtocolUtils.packDataToK900(
|
|
8402
8793
|
cmdObject.toString().toByteArray(StandardCharsets.UTF_8),
|
|
8403
|
-
K900ProtocolUtils.CMD_TYPE_STRING
|
|
8794
|
+
K900ProtocolUtils.CMD_TYPE_STRING,
|
|
8795
|
+
k900LengthEndian()
|
|
8404
8796
|
)
|
|
8405
8797
|
if (packedData == null) {
|
|
8406
8798
|
Bridge.log("LIVE: Failed to pack Voice Activity Detection setting command")
|