@mentra/bluetooth-sdk 0.1.19 → 0.1.21-beta.0
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 +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +239 -52
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +65 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +47 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +6 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +381 -97
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +293 -174
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +4 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +15 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +65 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +6 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +31 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +9 -0
- package/build/BluetoothSdk.types.d.ts +71 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +27 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +20 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
- package/build/_private/MentraLocalNetworkModule.js +3 -0
- package/build/_private/MentraLocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +1 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +2 -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 +9 -0
- package/build/index.js.map +1 -1
- package/expo-module.config.json +1 -0
- package/ios/BluetoothSdkModule.swift +72 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +72 -18
- package/ios/Source/DeviceManager.swift +52 -3
- package/ios/Source/DeviceStore.swift +6 -1
- package/ios/Source/MentraBluetoothSDK.swift +298 -21
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +22 -3
- package/ios/Source/sgcs/G1.swift +2 -2
- package/ios/Source/sgcs/G2.swift +19 -7
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +456 -52
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/Nimo.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +5 -2
- package/ios/Source/sgcs/Simulated.swift +1 -1
- package/ios/Source/status/WifiHotspotStatus.swift +21 -2
- package/ios/Source/streaming/StreamModels.swift +9 -0
- package/ios/Source/tts/TTSTools.swift +1 -1
- package/ios/Source/utils/G1Text.swift +30 -1
- package/package.json +1 -2
- package/src/BluetoothSdk.types.ts +75 -0
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +68 -13
- package/src/_private/MentraLocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +1 -0
- package/src/_private/photoRequestPayload.ts +3 -3
- package/src/index.ts +10 -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
|
|
@@ -33,6 +34,7 @@ import com.mentra.bluetoothsdk.Bridge
|
|
|
33
34
|
import com.mentra.bluetoothsdk.DeviceManager
|
|
34
35
|
import com.mentra.bluetoothsdk.PhotoRequest
|
|
35
36
|
import com.mentra.bluetoothsdk.PhotoSize
|
|
37
|
+
import com.mentra.bluetoothsdk.PhotoMode
|
|
36
38
|
import com.mentra.bluetoothsdk.DeviceStore
|
|
37
39
|
import com.mentra.bluetoothsdk.ObservableStore
|
|
38
40
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
@@ -99,6 +101,7 @@ class MentraLive : SGCManager() {
|
|
|
99
101
|
// PSM. When the phone opens the channel, the glasses send file packets over it instead
|
|
100
102
|
// of GATT FILE_READ notifications. Phone→glasses traffic stays on GATT.
|
|
101
103
|
private const val L2CAP_FILE_PSM = 0x00C9
|
|
104
|
+
private const val FILE_PACKET_LOG_INTERVAL = 32
|
|
102
105
|
|
|
103
106
|
// BLE UUIDs - updated to match K900 BES2800 MCU UUIDs for compatibility with both glass
|
|
104
107
|
// types
|
|
@@ -144,6 +147,10 @@ class MentraLive : SGCManager() {
|
|
|
144
147
|
|
|
145
148
|
// Heartbeat parameters
|
|
146
149
|
private const val HEARTBEAT_INTERVAL_MS = 30000 // 30 seconds
|
|
150
|
+
// Grace period before an unanswered BLE wire v2 handshake is re-armed for retry,
|
|
151
|
+
// and the per-session cap on automatic retries (see sendWireHandshake).
|
|
152
|
+
private const val WIRE_HANDSHAKE_RETRY_GRACE_MS = 5000L
|
|
153
|
+
private const val WIRE_HANDSHAKE_MAX_ATTEMPTS = 3
|
|
147
154
|
private const val BATTERY_REQUEST_EVERY_N_HEARTBEATS = 10 // Every 10 heartbeats (5 minutes)
|
|
148
155
|
private const val RSSI_READ_INTERVAL_MS = 10000L // 10 seconds
|
|
149
156
|
|
|
@@ -211,11 +218,21 @@ class MentraLive : SGCManager() {
|
|
|
211
218
|
private var peerWireProtocolVersion = 0
|
|
212
219
|
private var useBinaryWireProtocol = false
|
|
213
220
|
private var wireHandshakeQueued = false
|
|
221
|
+
private var wireHandshakeAttempts = 0
|
|
222
|
+
// Session generation in which the LAST outgoing v2 handshake was sent. A handshake
|
|
223
|
+
// reply only activates v2 when it answers a handshake from the CURRENT epoch - a
|
|
224
|
+
// stale handshake that survived in flight across an epoch reset must not flip the
|
|
225
|
+
// new session to v2 before it negotiated its own build and capabilities.
|
|
226
|
+
private var wireHandshakeSentGeneration = -1
|
|
227
|
+
// Bumped on every BLE session reset; scheduled handshake-retry callbacks capture it
|
|
228
|
+
// and no-op if the session changed, so a timer from a dead session can't poke a new one.
|
|
229
|
+
private var wireSessionGeneration = 0
|
|
214
230
|
// Negotiated K900 STRING length endianness for the phone<->glasses BLE link. Defaults to
|
|
215
231
|
// legacy big-endian; upgraded to little-endian only when the glasses advertise wire_caps.k900_le
|
|
216
232
|
// (or a v2 binary handshake succeeds, which implies wire-v2 LE).
|
|
217
233
|
private var peerK900Le = false
|
|
218
234
|
private var peerWireCapsBinary = false
|
|
235
|
+
private var peerFilePayloadV2 = false
|
|
219
236
|
// Note: appVersion, buildNumber, deviceModel, androidVersion
|
|
220
237
|
// are inherited from SGCManager parent class
|
|
221
238
|
|
|
@@ -242,6 +259,9 @@ class MentraLive : SGCManager() {
|
|
|
242
259
|
private var lc3ReadCharacteristic: BluetoothGattCharacteristic? = null
|
|
243
260
|
private var lc3WriteCharacteristic: BluetoothGattCharacteristic? = null
|
|
244
261
|
private var handler = Handler(Looper.getMainLooper())
|
|
262
|
+
private val fileProcessingThread =
|
|
263
|
+
HandlerThread("MentraLive-FileProcessing").apply { start() }
|
|
264
|
+
private val fileProcessingHandler = Handler(fileProcessingThread.looper)
|
|
245
265
|
private var scheduler: ScheduledExecutorService? = null
|
|
246
266
|
private var isScanning = false
|
|
247
267
|
private var isConnecting = false
|
|
@@ -307,6 +327,8 @@ class MentraLive : SGCManager() {
|
|
|
307
327
|
// So we temporarily suspend the LC3 mic during phone audio playback
|
|
308
328
|
private var micIntentEnabled = false // User/system WANTS mic enabled
|
|
309
329
|
private var micSuspendedForAudio = false // Mic temporarily suspended due to phone audio
|
|
330
|
+
override val isMicSuspendedForAudio: Boolean
|
|
331
|
+
get() = micSuspendedForAudio
|
|
310
332
|
private var phoneAudioMonitor: PhoneAudioMonitor? = null
|
|
311
333
|
private var micOnCount = 0
|
|
312
334
|
private var micOffCount = 0
|
|
@@ -321,7 +343,7 @@ class MentraLive : SGCManager() {
|
|
|
321
343
|
private var activeFileTransfers = ConcurrentHashMap<String, FileTransferSession>()
|
|
322
344
|
|
|
323
345
|
// BLE photo transfer tracking
|
|
324
|
-
private var blePhotoTransfers: MutableMap<String, BlePhotoTransfer> =
|
|
346
|
+
private var blePhotoTransfers: MutableMap<String, BlePhotoTransfer> = ConcurrentHashMap()
|
|
325
347
|
|
|
326
348
|
/** Expected incident log relay files from glasses (B… firmware, L… logcat). */
|
|
327
349
|
private val bleIncidentLogRelays = ConcurrentHashMap<String, BleIncidentLogRelay>()
|
|
@@ -774,13 +796,12 @@ class MentraLive : SGCManager() {
|
|
|
774
796
|
val isEqual = state == connectionState
|
|
775
797
|
if (isEqual) {
|
|
776
798
|
if (state == ConnTypes.DISCONNECTED) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
BleJsonCompact.resetSession()
|
|
799
|
+
resetWireNegotiationState()
|
|
800
|
+
// Queued writes are session-bound: transmitting them into the NEXT session
|
|
801
|
+
// (e.g. a stale handshake whose reply activates v2 before the new session
|
|
802
|
+
// negotiated) is the bug class this clear removes. Higher layers re-send
|
|
803
|
+
// what still matters via their own ACK/retry tracking.
|
|
804
|
+
sendQueue.clear()
|
|
784
805
|
}
|
|
785
806
|
return
|
|
786
807
|
}
|
|
@@ -808,13 +829,9 @@ class MentraLive : SGCManager() {
|
|
|
808
829
|
DeviceStore.apply("glasses", "connected", false)
|
|
809
830
|
DeviceStore.apply("glasses", "signalStrength", -1)
|
|
810
831
|
DeviceStore.apply("glasses", "signalStrengthUpdatedAt", 0L)
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
wireHandshakeQueued = false
|
|
815
|
-
peerK900Le = false
|
|
816
|
-
peerWireCapsBinary = false
|
|
817
|
-
BleJsonCompact.resetSession()
|
|
832
|
+
resetWireNegotiationState()
|
|
833
|
+
sendQueue.clear() // see the disconnect reset above: stale writes die with the session
|
|
834
|
+
|
|
818
835
|
// Drop OTA caches when fully disconnected — avoids leaking session/step state
|
|
819
836
|
// from a previous pairing into the next one.
|
|
820
837
|
resetOtaCache()
|
|
@@ -1440,6 +1457,15 @@ class MentraLive : SGCManager() {
|
|
|
1440
1457
|
)
|
|
1441
1458
|
}
|
|
1442
1459
|
|
|
1460
|
+
private fun phyLabel(phy: Int): String {
|
|
1461
|
+
return when (phy) {
|
|
1462
|
+
BluetoothDevice.PHY_LE_1M -> "1M"
|
|
1463
|
+
BluetoothDevice.PHY_LE_2M -> "2M"
|
|
1464
|
+
BluetoothDevice.PHY_LE_CODED -> "coded"
|
|
1465
|
+
else -> "unknown($phy)"
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1443
1469
|
/** GATT callback for BLE operations */
|
|
1444
1470
|
private val gattCallback: BluetoothGattCallback =
|
|
1445
1471
|
object : BluetoothGattCallback() {
|
|
@@ -1463,25 +1489,6 @@ class MentraLive : SGCManager() {
|
|
|
1463
1489
|
isConnected = true
|
|
1464
1490
|
connectedDevice = gatt.device
|
|
1465
1491
|
|
|
1466
|
-
// High-priority connection interval (~11-15ms vs 30-50ms
|
|
1467
|
-
// default) and 2M PHY, as MentraNex and G2 already do. The
|
|
1468
|
-
// BES firmware only *prefers* 2M PHY; the central has to
|
|
1469
|
-
// request the switch or the link stays on 1M.
|
|
1470
|
-
try {
|
|
1471
|
-
gatt.requestConnectionPriority(
|
|
1472
|
-
BluetoothGatt.CONNECTION_PRIORITY_HIGH
|
|
1473
|
-
)
|
|
1474
|
-
gatt.setPreferredPhy(
|
|
1475
|
-
BluetoothDevice.PHY_LE_2M_MASK,
|
|
1476
|
-
BluetoothDevice.PHY_LE_2M_MASK,
|
|
1477
|
-
BluetoothDevice.PHY_OPTION_NO_PREFERRED
|
|
1478
|
-
)
|
|
1479
|
-
} catch (e: SecurityException) {
|
|
1480
|
-
Bridge.log(
|
|
1481
|
-
"LIVE: requestConnectionPriority/setPreferredPhy denied: ${e.message}"
|
|
1482
|
-
)
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
1492
|
DeviceStore.apply("glasses", "bluetoothName", connectedDevice!!.name)
|
|
1486
1493
|
// Persist MAC so reconnection can use direct GATT instead of scanning
|
|
1487
1494
|
if (connectedDevice!!.address != null) {
|
|
@@ -1580,6 +1587,8 @@ class MentraLive : SGCManager() {
|
|
|
1580
1587
|
|
|
1581
1588
|
// Close the L2CAP file channel (if the fast path was open)
|
|
1582
1589
|
closeL2capFileChannel()
|
|
1590
|
+
fileProcessingHandler.removeCallbacksAndMessages(null)
|
|
1591
|
+
clearFilePacketBuffer()
|
|
1583
1592
|
|
|
1584
1593
|
// Clean up GATT resources
|
|
1585
1594
|
closeGattQuietly(false)
|
|
@@ -1635,6 +1644,8 @@ class MentraLive : SGCManager() {
|
|
|
1635
1644
|
|
|
1636
1645
|
// Close the L2CAP file channel (if the fast path was open)
|
|
1637
1646
|
closeL2capFileChannel()
|
|
1647
|
+
fileProcessingHandler.removeCallbacksAndMessages(null)
|
|
1648
|
+
clearFilePacketBuffer()
|
|
1638
1649
|
|
|
1639
1650
|
// Clean up resources
|
|
1640
1651
|
closeGattQuietly(false)
|
|
@@ -1647,6 +1658,38 @@ class MentraLive : SGCManager() {
|
|
|
1647
1658
|
}
|
|
1648
1659
|
}
|
|
1649
1660
|
|
|
1661
|
+
override fun onPhyUpdate(
|
|
1662
|
+
gatt: BluetoothGatt,
|
|
1663
|
+
txPhy: Int,
|
|
1664
|
+
rxPhy: Int,
|
|
1665
|
+
status: Int
|
|
1666
|
+
) {
|
|
1667
|
+
Bridge.log(
|
|
1668
|
+
"LIVE: PHY update tx=" +
|
|
1669
|
+
phyLabel(txPhy) +
|
|
1670
|
+
", rx=" +
|
|
1671
|
+
phyLabel(rxPhy) +
|
|
1672
|
+
", status=" +
|
|
1673
|
+
status
|
|
1674
|
+
)
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
override fun onPhyRead(
|
|
1678
|
+
gatt: BluetoothGatt,
|
|
1679
|
+
txPhy: Int,
|
|
1680
|
+
rxPhy: Int,
|
|
1681
|
+
status: Int
|
|
1682
|
+
) {
|
|
1683
|
+
Bridge.log(
|
|
1684
|
+
"LIVE: PHY read tx=" +
|
|
1685
|
+
phyLabel(txPhy) +
|
|
1686
|
+
", rx=" +
|
|
1687
|
+
phyLabel(rxPhy) +
|
|
1688
|
+
", status=" +
|
|
1689
|
+
status
|
|
1690
|
+
)
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1650
1693
|
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
1651
1694
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
1652
1695
|
Bridge.log("LIVE: GATT services discovered")
|
|
@@ -2631,18 +2674,6 @@ class MentraLive : SGCManager() {
|
|
|
2631
2674
|
var iterations = 0
|
|
2632
2675
|
val MAX_ITERATIONS = 100
|
|
2633
2676
|
|
|
2634
|
-
// Debug: Log hex dump of first 40 bytes
|
|
2635
|
-
val hexFirst = StringBuilder()
|
|
2636
|
-
for (i in 0 until Math.min(40, filePacketBufferSize)) {
|
|
2637
|
-
hexFirst.append(String.format("%02X ", filePacketBuffer[i]))
|
|
2638
|
-
}
|
|
2639
|
-
Bridge.log(
|
|
2640
|
-
"LIVE: 📦 extractCompleteFilePackets: buffer has " +
|
|
2641
|
-
filePacketBufferSize +
|
|
2642
|
-
" bytes, first 40: " +
|
|
2643
|
-
hexFirst.toString()
|
|
2644
|
-
)
|
|
2645
|
-
|
|
2646
2677
|
while (pos < filePacketBufferSize && iterations++ < MAX_ITERATIONS) {
|
|
2647
2678
|
// Find start marker ## (0x23 0x23)
|
|
2648
2679
|
var startPos = -1
|
|
@@ -2677,11 +2708,6 @@ class MentraLive : SGCManager() {
|
|
|
2677
2708
|
|
|
2678
2709
|
// Need at least 5 bytes to read type and packSize: ## (2) + type (1) + packSize (2)
|
|
2679
2710
|
if (filePacketBufferSize - pos < 5) {
|
|
2680
|
-
Bridge.log(
|
|
2681
|
-
"LIVE: 📦 Not enough data for header, have " +
|
|
2682
|
-
(filePacketBufferSize - pos) +
|
|
2683
|
-
" bytes, need 5"
|
|
2684
|
-
)
|
|
2685
2711
|
break
|
|
2686
2712
|
}
|
|
2687
2713
|
|
|
@@ -2691,33 +2717,9 @@ class MentraLive : SGCManager() {
|
|
|
2691
2717
|
((filePacketBuffer[packSizeOffset].toInt() and 0xFF) shl 8) or
|
|
2692
2718
|
(filePacketBuffer[packSizeOffset + 1].toInt() and 0xFF)
|
|
2693
2719
|
|
|
2694
|
-
// Also try little-endian for comparison
|
|
2695
|
-
val packSizeLE =
|
|
2696
|
-
(filePacketBuffer[packSizeOffset].toInt() and 0xFF) or
|
|
2697
|
-
((filePacketBuffer[packSizeOffset + 1].toInt() and 0xFF) shl 8)
|
|
2698
|
-
Bridge.log(
|
|
2699
|
-
"LIVE: 📦 Header bytes 3-4: 0x" +
|
|
2700
|
-
String.format(
|
|
2701
|
-
"%02X%02X",
|
|
2702
|
-
filePacketBuffer[packSizeOffset],
|
|
2703
|
-
filePacketBuffer[packSizeOffset + 1]
|
|
2704
|
-
) +
|
|
2705
|
-
" -> packSize BE=" +
|
|
2706
|
-
packSize +
|
|
2707
|
-
", LE=" +
|
|
2708
|
-
packSizeLE
|
|
2709
|
-
)
|
|
2710
|
-
|
|
2711
2720
|
// Validate packSize
|
|
2712
2721
|
if (packSize < 0 || packSize > K900ProtocolUtils.FILE_PACK_SIZE) {
|
|
2713
|
-
Log.w(
|
|
2714
|
-
TAG,
|
|
2715
|
-
"Invalid packSize " +
|
|
2716
|
-
packSize +
|
|
2717
|
-
" (LE would be " +
|
|
2718
|
-
packSizeLE +
|
|
2719
|
-
"), skipping start marker"
|
|
2720
|
-
)
|
|
2722
|
+
Log.w(TAG, "Invalid packSize $packSize, skipping start marker")
|
|
2721
2723
|
pos = startPos + 1
|
|
2722
2724
|
continue
|
|
2723
2725
|
}
|
|
@@ -2730,17 +2732,7 @@ class MentraLive : SGCManager() {
|
|
|
2730
2732
|
// Check if we have the complete packet
|
|
2731
2733
|
val availableBytes = filePacketBufferSize - pos
|
|
2732
2734
|
if (availableBytes < expectedPacketSize) {
|
|
2733
|
-
|
|
2734
|
-
Bridge.log(
|
|
2735
|
-
"LIVE: 📦 Waiting for more data: have " +
|
|
2736
|
-
availableBytes +
|
|
2737
|
-
" of " +
|
|
2738
|
-
expectedPacketSize +
|
|
2739
|
-
" bytes (packSize=" +
|
|
2740
|
-
packSize +
|
|
2741
|
-
")"
|
|
2742
|
-
)
|
|
2743
|
-
break // IMPORTANT: break here, don't continue looking for end marker
|
|
2735
|
+
break
|
|
2744
2736
|
}
|
|
2745
2737
|
|
|
2746
2738
|
// Verify end marker $$ at expected position
|
|
@@ -2748,25 +2740,6 @@ class MentraLive : SGCManager() {
|
|
|
2748
2740
|
val endByte1 = filePacketBuffer[endMarkerPos]
|
|
2749
2741
|
val endByte2 = filePacketBuffer[endMarkerPos + 1]
|
|
2750
2742
|
|
|
2751
|
-
// Debug: Show bytes around expected end marker position
|
|
2752
|
-
val endContext = StringBuilder()
|
|
2753
|
-
for (i in
|
|
2754
|
-
Math.max(0, endMarkerPos - 5)..Math.min(
|
|
2755
|
-
filePacketBufferSize - 1,
|
|
2756
|
-
endMarkerPos + 5
|
|
2757
|
-
)) {
|
|
2758
|
-
if (i == endMarkerPos) endContext.append("[")
|
|
2759
|
-
endContext.append(String.format("%02X", filePacketBuffer[i]))
|
|
2760
|
-
if (i == endMarkerPos + 1) endContext.append("]")
|
|
2761
|
-
endContext.append(" ")
|
|
2762
|
-
}
|
|
2763
|
-
Bridge.log(
|
|
2764
|
-
"LIVE: 📦 End marker check at pos " +
|
|
2765
|
-
endMarkerPos +
|
|
2766
|
-
": " +
|
|
2767
|
-
endContext.toString()
|
|
2768
|
-
)
|
|
2769
|
-
|
|
2770
2743
|
if (endByte1 != 0x24.toByte() || endByte2 != 0x24.toByte()) {
|
|
2771
2744
|
// End marker not found - could be corrupted packet or wrong packSize interpretation
|
|
2772
2745
|
Log.w(
|
|
@@ -2791,22 +2764,10 @@ class MentraLive : SGCManager() {
|
|
|
2791
2764
|
val completePacket = ByteArray(expectedPacketSize)
|
|
2792
2765
|
System.arraycopy(filePacketBuffer, pos, completePacket, 0, expectedPacketSize)
|
|
2793
2766
|
|
|
2794
|
-
Bridge.log(
|
|
2795
|
-
"LIVE: 📦 ✅ Complete file packet reassembled: " + expectedPacketSize + " bytes"
|
|
2796
|
-
)
|
|
2797
|
-
|
|
2798
2767
|
// Process the complete packet
|
|
2799
2768
|
val packetInfo = K900ProtocolUtils.extractFilePacket(completePacket)
|
|
2800
2769
|
if (packetInfo != null && packetInfo.isValid) {
|
|
2801
|
-
|
|
2802
|
-
"LIVE: 📦 ✅ Packet validated: index=" +
|
|
2803
|
-
packetInfo.packIndex +
|
|
2804
|
-
", fileName=" +
|
|
2805
|
-
packetInfo.fileName
|
|
2806
|
-
)
|
|
2807
|
-
// Post to handler to process outside the lock
|
|
2808
|
-
val finalPacketInfo = packetInfo
|
|
2809
|
-
handler.post { processFilePacket(finalPacketInfo) }
|
|
2770
|
+
enqueueFilePacket(packetInfo)
|
|
2810
2771
|
} else {
|
|
2811
2772
|
Log.e(TAG, "Failed to extract/validate reassembled file packet")
|
|
2812
2773
|
}
|
|
@@ -2819,13 +2780,6 @@ class MentraLive : SGCManager() {
|
|
|
2819
2780
|
val remaining = filePacketBufferSize - pos
|
|
2820
2781
|
System.arraycopy(filePacketBuffer, pos, filePacketBuffer, 0, remaining)
|
|
2821
2782
|
filePacketBufferSize = remaining
|
|
2822
|
-
Bridge.log(
|
|
2823
|
-
"LIVE: 📦 Removed " +
|
|
2824
|
-
pos +
|
|
2825
|
-
" bytes, " +
|
|
2826
|
-
remaining +
|
|
2827
|
-
" bytes remaining in buffer"
|
|
2828
|
-
)
|
|
2829
2783
|
} else if (pos >= filePacketBufferSize) {
|
|
2830
2784
|
filePacketBufferSize = 0
|
|
2831
2785
|
}
|
|
@@ -2942,7 +2896,7 @@ class MentraLive : SGCManager() {
|
|
|
2942
2896
|
// structure
|
|
2943
2897
|
val packetInfo = K900ProtocolUtils.extractFilePacket(data)
|
|
2944
2898
|
if (packetInfo != null && packetInfo.isValid) {
|
|
2945
|
-
|
|
2899
|
+
enqueueFilePacket(packetInfo)
|
|
2946
2900
|
} else {
|
|
2947
2901
|
Log.e(TAG, "Thread-" + threadId + ": Failed to extract or validate file packet")
|
|
2948
2902
|
// BES chip handles ACKs automatically
|
|
@@ -3191,7 +3145,12 @@ class MentraLive : SGCManager() {
|
|
|
3191
3145
|
DeviceStore.apply("glasses", "wifiError", "")
|
|
3192
3146
|
}
|
|
3193
3147
|
|
|
3194
|
-
updateWifiStatus(
|
|
3148
|
+
updateWifiStatus(
|
|
3149
|
+
wifiConnectedStatus,
|
|
3150
|
+
ssid,
|
|
3151
|
+
localIp,
|
|
3152
|
+
wifiError.takeIf { it.isNotEmpty() }
|
|
3153
|
+
)
|
|
3195
3154
|
}
|
|
3196
3155
|
"hotspot_status_update" -> {
|
|
3197
3156
|
// Process hotspot status information (same pattern as "wifi_status")
|
|
@@ -3294,7 +3253,8 @@ class MentraLive : SGCManager() {
|
|
|
3294
3253
|
|
|
3295
3254
|
val scanComplete =
|
|
3296
3255
|
json.optBoolean("scan_complete", json.optBoolean("scanComplete", false))
|
|
3297
|
-
|
|
3256
|
+
val scanId = json.optString("scanId", "").ifEmpty { null }
|
|
3257
|
+
Bridge.updateWifiScanResults(networks, scanComplete, scanId)
|
|
3298
3258
|
}
|
|
3299
3259
|
"token_status" -> {
|
|
3300
3260
|
// Process coreToken acknowledgment
|
|
@@ -3547,8 +3507,14 @@ class MentraLive : SGCManager() {
|
|
|
3547
3507
|
// Glasses SOC has booted and is ready for communication
|
|
3548
3508
|
Bridge.log("LIVE: 🎉 Received glasses_ready message - SOC is booted and ready!")
|
|
3549
3509
|
|
|
3550
|
-
//
|
|
3510
|
+
// glasses_ready is a REMOTE wire-session reset: the glasses ran
|
|
3511
|
+
// onTransportReset() before sending it, so their side is back on legacy
|
|
3512
|
+
// framing regardless of what this side negotiated earlier. Start a fresh
|
|
3513
|
+
// wire epoch (clears v2-active state that would otherwise gate the
|
|
3514
|
+
// handshake off), then negotiate from the caps this message advertises.
|
|
3515
|
+
resetWireNegotiationState()
|
|
3551
3516
|
parsePeerWireCaps(json)
|
|
3517
|
+
maybeSendWireHandshake()
|
|
3552
3518
|
|
|
3553
3519
|
// Set the ready flag to stop any future readiness checks
|
|
3554
3520
|
glassesReady = true
|
|
@@ -3559,6 +3525,7 @@ class MentraLive : SGCManager() {
|
|
|
3559
3525
|
|
|
3560
3526
|
// Stop the readiness check loop since we got confirmation
|
|
3561
3527
|
stopReadinessCheckLoop()
|
|
3528
|
+
advertiseFilePayloadCapabilityToBes()
|
|
3562
3529
|
|
|
3563
3530
|
// Try to open the L2CAP CoC fast path for file transfers. No-op when the
|
|
3564
3531
|
// firmware doesn't support it — GATT notifications remain the default path.
|
|
@@ -3938,8 +3905,6 @@ class MentraLive : SGCManager() {
|
|
|
3938
3905
|
val buildNumInt = Integer.parseInt(buildNum)
|
|
3939
3906
|
buildNumberInt = buildNumInt
|
|
3940
3907
|
Bridge.log("LIVE: Parsed build number as integer: " + buildNumInt)
|
|
3941
|
-
parsePeerWireCaps(json)
|
|
3942
|
-
maybeSendWireHandshake()
|
|
3943
3908
|
} catch (e: NumberFormatException) {
|
|
3944
3909
|
buildNumberInt = 0
|
|
3945
3910
|
Log.e(TAG, "Failed to parse build number as integer: " + buildNum)
|
|
@@ -4007,6 +3972,16 @@ class MentraLive : SGCManager() {
|
|
|
4007
3972
|
}
|
|
4008
3973
|
}
|
|
4009
3974
|
|
|
3975
|
+
// Negotiate wire caps from ANY version_info chunk, not only the one
|
|
3976
|
+
// carrying build_number: the glasses put wire_caps in version_info_3
|
|
3977
|
+
// (firmware chunk) while build_number travels in version_info_1, so
|
|
3978
|
+
// gating negotiation on build_number silently discards the caps.
|
|
3979
|
+
// parsePeerWireCaps no-ops without a wire_caps key and
|
|
3980
|
+
// maybeSendWireHandshake gates on caps + build + not-yet-queued, so
|
|
3981
|
+
// running them per chunk is safe.
|
|
3982
|
+
parsePeerWireCaps(json)
|
|
3983
|
+
maybeSendWireHandshake()
|
|
3984
|
+
|
|
4010
3985
|
Bridge.log("LIVE: Processed version_info fields and sent to RN")
|
|
4011
3986
|
Bridge.sendVersionInfo(fields)
|
|
4012
3987
|
} else {
|
|
@@ -4511,7 +4486,20 @@ class MentraLive : SGCManager() {
|
|
|
4511
4486
|
|
|
4512
4487
|
val syntheticStatus: String
|
|
4513
4488
|
if ("FINISHED" == besOtaStatus) {
|
|
4514
|
-
|
|
4489
|
+
// The glasses power-cycle right after the final BES tick, so a
|
|
4490
|
+
// session whose BES step is the LAST step never gets a follow-up
|
|
4491
|
+
// ota_status from the glasses — consumers mapping on this synthetic
|
|
4492
|
+
// status would otherwise never see a terminal state. Emit "complete"
|
|
4493
|
+
// for the final step; mid-session BES steps keep "step_complete" so
|
|
4494
|
+
// session-level trackers advance normally. Unknown sessions
|
|
4495
|
+
// (cachedOtaTotalSteps == 0, e.g. legacy glasses that never sent an
|
|
4496
|
+
// ota_status) conservatively keep "step_complete".
|
|
4497
|
+
syntheticStatus =
|
|
4498
|
+
if (cachedOtaTotalSteps > 0 &&
|
|
4499
|
+
cachedOtaCurrentStep >= cachedOtaTotalSteps
|
|
4500
|
+
)
|
|
4501
|
+
"complete"
|
|
4502
|
+
else "step_complete"
|
|
4515
4503
|
} else if ("FAILED" == besOtaStatus) {
|
|
4516
4504
|
syntheticStatus = "failed"
|
|
4517
4505
|
} else {
|
|
@@ -4755,7 +4743,12 @@ class MentraLive : SGCManager() {
|
|
|
4755
4743
|
/**
|
|
4756
4744
|
* Update WiFi status and notify listeners Matches iOS MentraLive.swift updateWifiStatus pattern
|
|
4757
4745
|
*/
|
|
4758
|
-
private fun updateWifiStatus(
|
|
4746
|
+
private fun updateWifiStatus(
|
|
4747
|
+
connected: Boolean,
|
|
4748
|
+
ssid: String,
|
|
4749
|
+
localIp: String,
|
|
4750
|
+
error: String? = null
|
|
4751
|
+
) {
|
|
4759
4752
|
Bridge.log("LIVE: 🌐 Updating WiFi status - connected: " + connected + ", SSID: " + ssid)
|
|
4760
4753
|
|
|
4761
4754
|
// Update parent SGCManager fields
|
|
@@ -4764,7 +4757,7 @@ class MentraLive : SGCManager() {
|
|
|
4764
4757
|
DeviceStore.apply("glasses", "wifiLocalIp", localIp)
|
|
4765
4758
|
|
|
4766
4759
|
// Send event to bridge for cloud communication
|
|
4767
|
-
Bridge.sendWifiStatusChange(connected, ssid, localIp)
|
|
4760
|
+
Bridge.sendWifiStatusChange(connected, ssid, localIp, error)
|
|
4768
4761
|
}
|
|
4769
4762
|
|
|
4770
4763
|
/**
|
|
@@ -4838,10 +4831,13 @@ class MentraLive : SGCManager() {
|
|
|
4838
4831
|
/**
|
|
4839
4832
|
* Request WiFi scan from the glasses This will ask the glasses to scan for available networks
|
|
4840
4833
|
*/
|
|
4841
|
-
override fun requestWifiScan() {
|
|
4834
|
+
override fun requestWifiScan(scanId: String?) {
|
|
4842
4835
|
try {
|
|
4843
4836
|
val json = JSONObject()
|
|
4844
4837
|
json.put("type", "request_wifi_scan")
|
|
4838
|
+
if (!scanId.isNullOrEmpty()) {
|
|
4839
|
+
json.put("scanId", scanId)
|
|
4840
|
+
}
|
|
4845
4841
|
sendJson(json, true)
|
|
4846
4842
|
Bridge.log("LIVE: Sending WiFi scan request to glasses")
|
|
4847
4843
|
} catch (e: JSONException) {
|
|
@@ -5431,6 +5427,7 @@ class MentraLive : SGCManager() {
|
|
|
5431
5427
|
override fun requestPhoto(request: PhotoRequest) {
|
|
5432
5428
|
val requestId = request.requestId
|
|
5433
5429
|
val size = request.size.value
|
|
5430
|
+
val mode = request.mode.value
|
|
5434
5431
|
val webhookUrl = request.webhookUrl
|
|
5435
5432
|
val authToken = request.authToken
|
|
5436
5433
|
val compress = request.compress.value
|
|
@@ -5444,6 +5441,8 @@ class MentraLive : SGCManager() {
|
|
|
5444
5441
|
requestId +
|
|
5445
5442
|
" with size: " +
|
|
5446
5443
|
size +
|
|
5444
|
+
", mode=" +
|
|
5445
|
+
mode +
|
|
5447
5446
|
", webhookUrl: " +
|
|
5448
5447
|
webhookUrl +
|
|
5449
5448
|
", authToken: " +
|
|
@@ -5481,6 +5480,7 @@ class MentraLive : SGCManager() {
|
|
|
5481
5480
|
if (size != null && !size.isEmpty()) {
|
|
5482
5481
|
json.put("size", size)
|
|
5483
5482
|
}
|
|
5483
|
+
json.put("mode", mode)
|
|
5484
5484
|
if (compress != null && !compress.isEmpty()) {
|
|
5485
5485
|
json.put("compress", compress)
|
|
5486
5486
|
} else {
|
|
@@ -5508,8 +5508,9 @@ class MentraLive : SGCManager() {
|
|
|
5508
5508
|
val bleImgId = "I" + String.format("%09d", System.currentTimeMillis() % 1000000000)
|
|
5509
5509
|
json.put("bleImgId", bleImgId)
|
|
5510
5510
|
|
|
5511
|
-
//
|
|
5512
|
-
|
|
5511
|
+
// Miniapps may explicitly skip direct Wi-Fi upload and force the
|
|
5512
|
+
// phone-relayed BLE path. Auto remains the default.
|
|
5513
|
+
json.put("transferMethod", request.transferMethod)
|
|
5513
5514
|
|
|
5514
5515
|
// Always prepare for potential BLE transfer
|
|
5515
5516
|
if (webhookUrl != null && !webhookUrl.isEmpty()) {
|
|
@@ -5519,9 +5520,11 @@ class MentraLive : SGCManager() {
|
|
|
5519
5520
|
blePhotoTransfers[bleImgId] = transfer
|
|
5520
5521
|
}
|
|
5521
5522
|
|
|
5522
|
-
Bridge.log("LIVE: Using
|
|
5523
|
+
Bridge.log("LIVE: Using " + request.transferMethod + " transfer mode with BLE fallback ID: " + bleImgId)
|
|
5523
5524
|
Bridge.log(
|
|
5524
|
-
"LIVE: PHOTO PIPELINE [5b/6] JSON ready
|
|
5525
|
+
"LIVE: PHOTO PIPELINE [5b/6] JSON ready mode=" +
|
|
5526
|
+
mode +
|
|
5527
|
+
" — " +
|
|
5525
5528
|
summarizeOutgoingMessage(json.toString()) +
|
|
5526
5529
|
", wakeup=true"
|
|
5527
5530
|
)
|
|
@@ -5536,6 +5539,7 @@ class MentraLive : SGCManager() {
|
|
|
5536
5539
|
fun warmUpCamera(
|
|
5537
5540
|
requestId: String,
|
|
5538
5541
|
size: PhotoSize,
|
|
5542
|
+
mode: PhotoMode = PhotoMode.PHOTO,
|
|
5539
5543
|
exposureTimeNs: Long?,
|
|
5540
5544
|
durationMs: Int,
|
|
5541
5545
|
) {
|
|
@@ -5544,6 +5548,8 @@ class MentraLive : SGCManager() {
|
|
|
5544
5548
|
requestId +
|
|
5545
5549
|
", size=" +
|
|
5546
5550
|
size.value +
|
|
5551
|
+
", mode=" +
|
|
5552
|
+
mode.value +
|
|
5547
5553
|
", durationMs=" +
|
|
5548
5554
|
durationMs
|
|
5549
5555
|
)
|
|
@@ -5554,6 +5560,7 @@ class MentraLive : SGCManager() {
|
|
|
5554
5560
|
json.put("requestId", requestId)
|
|
5555
5561
|
val sizeValue = size.value
|
|
5556
5562
|
json.put("size", if (sizeValue.isNotEmpty()) sizeValue else "medium")
|
|
5563
|
+
json.put("mode", mode.value)
|
|
5557
5564
|
if (exposureTimeNs != null && exposureTimeNs > 0L) {
|
|
5558
5565
|
json.put("exposureTimeNs", exposureTimeNs)
|
|
5559
5566
|
}
|
|
@@ -5564,6 +5571,16 @@ class MentraLive : SGCManager() {
|
|
|
5564
5571
|
}
|
|
5565
5572
|
}
|
|
5566
5573
|
|
|
5574
|
+
fun stopCameraWarmUp(requestId: String) {
|
|
5575
|
+
if (!isConnected) {
|
|
5576
|
+
throw IllegalStateException("not_connected")
|
|
5577
|
+
}
|
|
5578
|
+
val json = JSONObject()
|
|
5579
|
+
json.put("type", "camera_warm_up_stop")
|
|
5580
|
+
json.put("requestId", requestId)
|
|
5581
|
+
sendJson(json, true)
|
|
5582
|
+
}
|
|
5583
|
+
|
|
5567
5584
|
override fun startStream(message: MutableMap<String, Any>) {
|
|
5568
5585
|
Bridge.log("LIVE: Starting RTMP stream")
|
|
5569
5586
|
|
|
@@ -6178,6 +6195,7 @@ class MentraLive : SGCManager() {
|
|
|
6178
6195
|
|
|
6179
6196
|
// Cancel any pending handlers
|
|
6180
6197
|
handler.removeCallbacksAndMessages(null)
|
|
6198
|
+
fileProcessingHandler.removeCallbacksAndMessages(null)
|
|
6181
6199
|
heartbeatHandler.removeCallbacksAndMessages(null)
|
|
6182
6200
|
rssiReadHandler.removeCallbacksAndMessages(null)
|
|
6183
6201
|
micBeatHandler.removeCallbacksAndMessages(null)
|
|
@@ -6209,6 +6227,7 @@ class MentraLive : SGCManager() {
|
|
|
6209
6227
|
|
|
6210
6228
|
// Clear file packet reassembly buffer
|
|
6211
6229
|
clearFilePacketBuffer()
|
|
6230
|
+
fileProcessingThread.quitSafely()
|
|
6212
6231
|
|
|
6213
6232
|
// Reset state variables
|
|
6214
6233
|
reconnectAttempts = 0
|
|
@@ -7208,6 +7227,28 @@ class MentraLive : SGCManager() {
|
|
|
7208
7227
|
* negotiated per-link endianness and binary support. Missing wire_caps leaves the legacy
|
|
7209
7228
|
* defaults (BE, no binary) untouched so older glasses keep working.
|
|
7210
7229
|
*/
|
|
7230
|
+
/**
|
|
7231
|
+
* Drop every negotiated wire-session artifact and bump the session generation so
|
|
7232
|
+
* scheduled callbacks from the old session stand down. Called on BLE disconnect and on
|
|
7233
|
+
* glasses_ready: the glasses run onTransportReset() before sending glasses_ready, so a
|
|
7234
|
+
* mid-link ASG restart (no BLE disconnect) resets THEIR side to legacy framing - the
|
|
7235
|
+
* phone must treat every glasses_ready as a new wire epoch and renegotiate (bounded by
|
|
7236
|
+
* the per-session handshake attempt cap) instead of trusting stale v2-active state.
|
|
7237
|
+
*/
|
|
7238
|
+
private fun resetWireNegotiationState() {
|
|
7239
|
+
incomingChunkReassembler.clear()
|
|
7240
|
+
peerWireProtocolVersion = 0
|
|
7241
|
+
useBinaryWireProtocol = false
|
|
7242
|
+
wireHandshakeQueued = false
|
|
7243
|
+
wireHandshakeAttempts = 0
|
|
7244
|
+
wireSessionGeneration++
|
|
7245
|
+
peerK900Le = false
|
|
7246
|
+
peerWireCapsBinary = false
|
|
7247
|
+
peerFilePayloadV2 = false
|
|
7248
|
+
BleJsonCompact.resetSession()
|
|
7249
|
+
wireHandshakeSentGeneration = -1
|
|
7250
|
+
}
|
|
7251
|
+
|
|
7211
7252
|
private fun parsePeerWireCaps(json: JSONObject) {
|
|
7212
7253
|
val caps = json.optJSONObject("wire_caps") ?: return
|
|
7213
7254
|
if (caps.optBoolean("k900_le", false)) {
|
|
@@ -7219,6 +7260,32 @@ class MentraLive : SGCManager() {
|
|
|
7219
7260
|
if (caps.has("binary")) {
|
|
7220
7261
|
peerWireCapsBinary = caps.optBoolean("binary", false)
|
|
7221
7262
|
}
|
|
7263
|
+
if (caps.has("file_payload_v2")) {
|
|
7264
|
+
peerFilePayloadV2 = caps.optBoolean("file_payload_v2", false)
|
|
7265
|
+
}
|
|
7266
|
+
}
|
|
7267
|
+
|
|
7268
|
+
private fun advertiseFilePayloadCapabilityToBes() {
|
|
7269
|
+
if (!peerFilePayloadV2) return
|
|
7270
|
+
try {
|
|
7271
|
+
val command = JSONObject()
|
|
7272
|
+
command.put("C", "cs_file_payload")
|
|
7273
|
+
command.put("B", "{\"max\":" + K900ProtocolUtils.FILE_PACK_SIZE + "}")
|
|
7274
|
+
val packed =
|
|
7275
|
+
K900ProtocolUtils.packDataToK900(
|
|
7276
|
+
command.toString().toByteArray(StandardCharsets.UTF_8),
|
|
7277
|
+
K900ProtocolUtils.CMD_TYPE_STRING,
|
|
7278
|
+
k900LengthEndian()
|
|
7279
|
+
)
|
|
7280
|
+
queueData(packed)
|
|
7281
|
+
Bridge.log(
|
|
7282
|
+
"LIVE: 📦 Advertised file payload ceiling " +
|
|
7283
|
+
K900ProtocolUtils.FILE_PACK_SIZE +
|
|
7284
|
+
" to BES"
|
|
7285
|
+
)
|
|
7286
|
+
} catch (e: JSONException) {
|
|
7287
|
+
Log.e(TAG, "Failed to advertise file payload capability", e)
|
|
7288
|
+
}
|
|
7222
7289
|
}
|
|
7223
7290
|
|
|
7224
7291
|
private fun maybeSendWireHandshake() {
|
|
@@ -7253,7 +7320,39 @@ class MentraLive : SGCManager() {
|
|
|
7253
7320
|
)
|
|
7254
7321
|
Bridge.log("LIVE: Sending BLE wire v2 handshake")
|
|
7255
7322
|
wireHandshakeQueued = true
|
|
7323
|
+
wireHandshakeSentGeneration = wireSessionGeneration
|
|
7256
7324
|
queueData(packed, null)
|
|
7325
|
+
// The handshake and its reply are fire-and-forget binary frames with no ACK
|
|
7326
|
+
// tracking; if either is lost, wireHandshakeQueued would block every future
|
|
7327
|
+
// attempt and the session would silently stay on the legacy string wire.
|
|
7328
|
+
// Re-arm after a grace period so negotiation can retry, bounded per session
|
|
7329
|
+
// so a peer that advertises binary but never answers doesn't get pinged
|
|
7330
|
+
// forever (later caps/version triggers may still retry explicitly).
|
|
7331
|
+
wireHandshakeAttempts++
|
|
7332
|
+
val scheduledGeneration = wireSessionGeneration
|
|
7333
|
+
handler.postDelayed(
|
|
7334
|
+
{
|
|
7335
|
+
if (scheduledGeneration == wireSessionGeneration &&
|
|
7336
|
+
wireHandshakeQueued &&
|
|
7337
|
+
peerWireProtocolVersion < BleWireProtocol.PROTOCOL_V2
|
|
7338
|
+
) {
|
|
7339
|
+
Bridge.log(
|
|
7340
|
+
"LIVE: BLE wire v2 handshake unanswered after " +
|
|
7341
|
+
WIRE_HANDSHAKE_RETRY_GRACE_MS +
|
|
7342
|
+
"ms (attempt " +
|
|
7343
|
+
wireHandshakeAttempts +
|
|
7344
|
+
"/" +
|
|
7345
|
+
WIRE_HANDSHAKE_MAX_ATTEMPTS +
|
|
7346
|
+
") - re-arming"
|
|
7347
|
+
)
|
|
7348
|
+
wireHandshakeQueued = false
|
|
7349
|
+
if (wireHandshakeAttempts < WIRE_HANDSHAKE_MAX_ATTEMPTS) {
|
|
7350
|
+
maybeSendWireHandshake()
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
},
|
|
7354
|
+
WIRE_HANDSHAKE_RETRY_GRACE_MS
|
|
7355
|
+
)
|
|
7257
7356
|
} catch (e: Exception) {
|
|
7258
7357
|
Log.e(TAG, "Failed to send wire handshake", e)
|
|
7259
7358
|
}
|
|
@@ -7263,12 +7362,17 @@ class MentraLive : SGCManager() {
|
|
|
7263
7362
|
peerWireProtocolVersion = BleWireProtocol.PROTOCOL_V2
|
|
7264
7363
|
useBinaryWireProtocol = true
|
|
7265
7364
|
wireHandshakeQueued = false
|
|
7365
|
+
wireHandshakeAttempts = 0
|
|
7266
7366
|
peerK900Le = true
|
|
7267
7367
|
BleJsonCompact.markSessionConnected(System.currentTimeMillis())
|
|
7268
7368
|
Bridge.log(logMessage)
|
|
7269
7369
|
}
|
|
7270
7370
|
|
|
7271
7371
|
private fun handlePeerWireHandshake() {
|
|
7372
|
+
if (wireHandshakeSentGeneration != wireSessionGeneration) {
|
|
7373
|
+
Bridge.log("LIVE: Ignoring wire v2 handshake reply from a previous session epoch")
|
|
7374
|
+
return
|
|
7375
|
+
}
|
|
7272
7376
|
activateBinaryWireV2Session("LIVE: Peer confirmed BLE wire protocol v2")
|
|
7273
7377
|
}
|
|
7274
7378
|
|
|
@@ -7793,7 +7897,7 @@ class MentraLive : SGCManager() {
|
|
|
7793
7897
|
|
|
7794
7898
|
private fun summarizeOutgoingMessage(payload: String?): String {
|
|
7795
7899
|
if (payload == null || payload.isEmpty()) {
|
|
7796
|
-
return "type=unknown, requestId=none, appId=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, iso=none, mId=none"
|
|
7900
|
+
return "type=unknown, requestId=none, appId=none, mode=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, iso=none, mId=none"
|
|
7797
7901
|
}
|
|
7798
7902
|
try {
|
|
7799
7903
|
val obj = JSONObject(payload)
|
|
@@ -7802,6 +7906,7 @@ class MentraLive : SGCManager() {
|
|
|
7802
7906
|
val appId = obj.optString("appId", "none")
|
|
7803
7907
|
val transferMethod = obj.optString("transferMethod", "none")
|
|
7804
7908
|
val bleImgId = obj.optString("bleImgId", "none")
|
|
7909
|
+
val mode = if (obj.has("mode")) obj.optString("mode", "photo") else "none"
|
|
7805
7910
|
val exposure =
|
|
7806
7911
|
if (obj.has("exposureTimeNs")) obj.optLong("exposureTimeNs").toString()
|
|
7807
7912
|
else "none"
|
|
@@ -7817,6 +7922,8 @@ class MentraLive : SGCManager() {
|
|
|
7817
7922
|
transferMethod +
|
|
7818
7923
|
", bleImgId=" +
|
|
7819
7924
|
bleImgId +
|
|
7925
|
+
", mode=" +
|
|
7926
|
+
mode +
|
|
7820
7927
|
", exposureTimeNs=" +
|
|
7821
7928
|
exposure +
|
|
7822
7929
|
", iso=" +
|
|
@@ -7964,7 +8071,7 @@ class MentraLive : SGCManager() {
|
|
|
7964
8071
|
val type = json.optString("type", "")
|
|
7965
8072
|
|
|
7966
8073
|
when (type) {
|
|
7967
|
-
"request_wifi_scan" -> requestWifiScan()
|
|
8074
|
+
"request_wifi_scan" -> requestWifiScan(null)
|
|
7968
8075
|
"rgb_led_control_on", "rgb_led_control_off" -> {
|
|
7969
8076
|
// Forward LED control commands directly to glasses via BLE
|
|
7970
8077
|
Log.d(TAG, "💡 Forwarding LED control command to glasses: " + type)
|
|
@@ -8126,6 +8233,11 @@ class MentraLive : SGCManager() {
|
|
|
8126
8233
|
// File Transfer Methods
|
|
8127
8234
|
// ---------------------------------------
|
|
8128
8235
|
|
|
8236
|
+
/** Keep file assembly ordered without blocking BLE callbacks or the Android main looper. */
|
|
8237
|
+
private fun enqueueFilePacket(packetInfo: K900ProtocolUtils.FilePacketInfo) {
|
|
8238
|
+
fileProcessingHandler.post { processFilePacket(packetInfo) }
|
|
8239
|
+
}
|
|
8240
|
+
|
|
8129
8241
|
/** Process a received file packet */
|
|
8130
8242
|
private fun processFilePacket(packetInfo: K900ProtocolUtils.FilePacketInfo) {
|
|
8131
8243
|
// Calculate total packets based on actual pack size (not hardcoded FILE_PACK_SIZE)
|
|
@@ -8133,18 +8245,15 @@ class MentraLive : SGCManager() {
|
|
|
8133
8245
|
if (packetInfo.packSize > 0)
|
|
8134
8246
|
(packetInfo.fileSize + packetInfo.packSize - 1) / packetInfo.packSize
|
|
8135
8247
|
else 1
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
packetInfo.packSize +
|
|
8146
|
-
" bytes)"
|
|
8147
|
-
)
|
|
8248
|
+
if (packetInfo.packIndex % FILE_PACKET_LOG_INTERVAL == 0 ||
|
|
8249
|
+
packetInfo.packIndex == totalPackets - 1
|
|
8250
|
+
) {
|
|
8251
|
+
Log.d(
|
|
8252
|
+
TAG,
|
|
8253
|
+
"File transfer ${packetInfo.fileName}: " +
|
|
8254
|
+
"${packetInfo.packIndex + 1}/$totalPackets packets"
|
|
8255
|
+
)
|
|
8256
|
+
}
|
|
8148
8257
|
|
|
8149
8258
|
// Check if this is a BLE photo transfer we're tracking
|
|
8150
8259
|
// The filename might have an extension (.avif or .jpg), but we track by ID only
|
|
@@ -8154,8 +8263,6 @@ class MentraLive : SGCManager() {
|
|
|
8154
8263
|
bleImgId = bleImgId.substring(0, dotIndex)
|
|
8155
8264
|
}
|
|
8156
8265
|
|
|
8157
|
-
Bridge.log("LIVE: 📦 BLE photo transfer packet for requestId: " + bleImgId)
|
|
8158
|
-
|
|
8159
8266
|
val incidentRelay = bleIncidentLogRelays[bleImgId]
|
|
8160
8267
|
if (incidentRelay != null) {
|
|
8161
8268
|
Bridge.log("LIVE: 📦 BLE incident log relay packet for: " + bleImgId)
|
|
@@ -8209,18 +8316,8 @@ class MentraLive : SGCManager() {
|
|
|
8209
8316
|
}
|
|
8210
8317
|
|
|
8211
8318
|
val photoTransfer = blePhotoTransfers[bleImgId]
|
|
8212
|
-
Bridge.log(
|
|
8213
|
-
"LIVE: 📦 BLE photo transfer for requestId: " +
|
|
8214
|
-
bleImgId +
|
|
8215
|
-
" found: " +
|
|
8216
|
-
(photoTransfer != null)
|
|
8217
|
-
)
|
|
8218
8319
|
if (photoTransfer != null) {
|
|
8219
8320
|
// This is a BLE photo transfer
|
|
8220
|
-
Bridge.log(
|
|
8221
|
-
"LIVE: 📦 BLE photo transfer packet for requestId: " + photoTransfer.requestId
|
|
8222
|
-
)
|
|
8223
|
-
|
|
8224
8321
|
// Get or create session for this transfer
|
|
8225
8322
|
if (photoTransfer.session == null) {
|
|
8226
8323
|
photoTransfer.session =
|
|
@@ -8332,13 +8429,6 @@ class MentraLive : SGCManager() {
|
|
|
8332
8429
|
val added = session.addPacket(packetInfo.packIndex, packetInfo.data)
|
|
8333
8430
|
|
|
8334
8431
|
if (added) {
|
|
8335
|
-
// BES chip handles ACKs automatically
|
|
8336
|
-
Bridge.log(
|
|
8337
|
-
"LIVE: 📦 Packet " +
|
|
8338
|
-
packetInfo.packIndex +
|
|
8339
|
-
" received successfully (BES will auto-ACK)"
|
|
8340
|
-
)
|
|
8341
|
-
|
|
8342
8432
|
// Check completion when final packet arrives or transfer is complete
|
|
8343
8433
|
if (session.shouldCheckCompletion(packetInfo.packIndex)) {
|
|
8344
8434
|
if (session.isComplete) {
|
|
@@ -8860,7 +8950,7 @@ class MentraLive : SGCManager() {
|
|
|
8860
8950
|
|
|
8861
8951
|
/** Send camera FOV setting to glasses (K900 / Mentra Live). */
|
|
8862
8952
|
override fun sendCameraFovSetting() {
|
|
8863
|
-
var fov =
|
|
8953
|
+
var fov = 102
|
|
8864
8954
|
var roiPosition = 0
|
|
8865
8955
|
try {
|
|
8866
8956
|
val raw = DeviceStore.get("bluetooth", "camera_fov")
|
|
@@ -8902,6 +8992,35 @@ class MentraLive : SGCManager() {
|
|
|
8902
8992
|
}
|
|
8903
8993
|
}
|
|
8904
8994
|
|
|
8995
|
+
fun sendCameraFovOverride(
|
|
8996
|
+
requestId: String,
|
|
8997
|
+
leaseId: String,
|
|
8998
|
+
fov: Int,
|
|
8999
|
+
roiPosition: Int,
|
|
9000
|
+
ttlMs: Int,
|
|
9001
|
+
) {
|
|
9002
|
+
val json = JSONObject()
|
|
9003
|
+
json.put("type", "camera_fov_override")
|
|
9004
|
+
json.put("request_id", requestId)
|
|
9005
|
+
json.put(
|
|
9006
|
+
"params",
|
|
9007
|
+
JSONObject()
|
|
9008
|
+
.put("lease_id", leaseId)
|
|
9009
|
+
.put("fov", fov)
|
|
9010
|
+
.put("roi_position", roiPosition)
|
|
9011
|
+
.put("ttl_ms", ttlMs),
|
|
9012
|
+
)
|
|
9013
|
+
sendJson(json, true)
|
|
9014
|
+
}
|
|
9015
|
+
|
|
9016
|
+
fun releaseCameraFovOverride(requestId: String, leaseId: String) {
|
|
9017
|
+
val json = JSONObject()
|
|
9018
|
+
json.put("type", "camera_fov_override_release")
|
|
9019
|
+
json.put("request_id", requestId)
|
|
9020
|
+
json.put("params", JSONObject().put("lease_id", leaseId))
|
|
9021
|
+
sendJson(json, true)
|
|
9022
|
+
}
|
|
9023
|
+
|
|
8905
9024
|
/**
|
|
8906
9025
|
* Send camera tuning config (ANR / gain) to the glasses via the {@code camera_tuning_config}
|
|
8907
9026
|
* command. The ASG client relays this as a {@code camconfig} broadcast to the camera HAL.
|