@mentra/bluetooth-sdk 3.3.0-dev.419 → 3.3.0-dev.421
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/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +17 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleEvidenceLog.kt +454 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +21 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +230 -32
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +41 -6
- package/android/src/test/java/com/mentra/bluetoothsdk/WifiScanEvidenceTest.kt +117 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleEvidenceLogTest.kt +282 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveEvidenceTest.kt +603 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/MessageChunkReassemblerOriginTest.kt +51 -0
- package/build/BluetoothSdk.types.d.ts +10 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +9 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +2 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/diagnosticRender.d.ts +12 -0
- package/build/_private/diagnosticRender.d.ts.map +1 -0
- package/build/_private/diagnosticRender.js +27 -0
- package/build/_private/diagnosticRender.js.map +1 -0
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/sgcs/MentraLive.swift +5 -1
- package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +10 -0
- package/src/_private/BluetoothSdkModule.ts +13 -0
- package/src/_private/diagnosticRender.ts +35 -0
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -43,6 +43,7 @@ import com.mentra.bluetoothsdk.DeviceStore
|
|
|
43
43
|
import com.mentra.bluetoothsdk.ObservableStore
|
|
44
44
|
import com.mentra.bluetoothsdk.incomingGlassesMessageAckId
|
|
45
45
|
import com.mentra.bluetoothsdk.wifiResponseEnvelopeIsValid
|
|
46
|
+
import com.mentra.bluetoothsdk.debug.BleEvidenceLog
|
|
46
47
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
47
48
|
import com.mentra.bluetoothsdk.utils.BlePhotoUploadService
|
|
48
49
|
import com.mentra.bluetoothsdk.utils.ConnTypes
|
|
@@ -597,6 +598,12 @@ class MentraLive : SGCManager() {
|
|
|
597
598
|
private val gattLifecycleHandler = Handler(Looper.getMainLooper())
|
|
598
599
|
// Owned exclusively by the main looper, including callback validation and mutation.
|
|
599
600
|
private var gattEpoch = 0L
|
|
601
|
+
// Provenance of the current accepted connection and wire epoch. Copied into each accepted
|
|
602
|
+
// notification before decoding; never re-read to label an already received message.
|
|
603
|
+
private var evidenceOrigin: BleEvidenceLog.Origin? = null
|
|
604
|
+
// Evidence boundary of the current bleSessionGeneration (seq of its ble_generation record).
|
|
605
|
+
@Volatile private var evidenceWriteGeneration = 0L
|
|
606
|
+
@Volatile private var evidenceWriteGenerationValue = -1L
|
|
600
607
|
private var connectionRequestEpoch = 0L
|
|
601
608
|
@Volatile private var gattTeardownToken: Long? = null
|
|
602
609
|
@Volatile private var gattTeardownTimeoutRunnable: Runnable? = null
|
|
@@ -650,7 +657,9 @@ class MentraLive : SGCManager() {
|
|
|
650
657
|
val commandType: String,
|
|
651
658
|
val requestId: String?,
|
|
652
659
|
val appId: String?,
|
|
653
|
-
val messageId: Long
|
|
660
|
+
val messageId: Long?,
|
|
661
|
+
// Wi-Fi scan request id; every native write of that command is recorded against it.
|
|
662
|
+
val scanId: String? = null
|
|
654
663
|
)
|
|
655
664
|
|
|
656
665
|
private data class BleWriteTrace(
|
|
@@ -666,7 +675,11 @@ class MentraLive : SGCManager() {
|
|
|
666
675
|
val packedBytes: Int,
|
|
667
676
|
val wakeup: Boolean,
|
|
668
677
|
val chunked: Boolean,
|
|
669
|
-
val queuedAtMs: Long
|
|
678
|
+
val queuedAtMs: Long,
|
|
679
|
+
val scanId: String? = null,
|
|
680
|
+
// Provenance captured when the write is queued; later outcomes never re-read state.
|
|
681
|
+
val evidenceConnection: Long = 0L,
|
|
682
|
+
val evidenceWriteGeneration: Long? = null
|
|
670
683
|
)
|
|
671
684
|
|
|
672
685
|
private data class QueuedBleWrite(
|
|
@@ -1024,6 +1037,7 @@ class MentraLive : SGCManager() {
|
|
|
1024
1037
|
this.type = DeviceTypes.LIVE
|
|
1025
1038
|
this.hasMic = true
|
|
1026
1039
|
this.context = Bridge.getContext()
|
|
1040
|
+
recordWriteGeneration(bleSessionGeneration.get())
|
|
1027
1041
|
|
|
1028
1042
|
// Initialize bluetooth adapter
|
|
1029
1043
|
val bluetoothManager =
|
|
@@ -1179,6 +1193,9 @@ class MentraLive : SGCManager() {
|
|
|
1179
1193
|
if (state == ConnTypes.DISCONNECTED) {
|
|
1180
1194
|
DeviceStore.apply("glasses", "hotspotOtaVersion", 0)
|
|
1181
1195
|
}
|
|
1196
|
+
if (state == ConnTypes.DISCONNECTED) {
|
|
1197
|
+
closeEvidenceConnection("disconnected")
|
|
1198
|
+
}
|
|
1182
1199
|
val isEqual = state == connectionState
|
|
1183
1200
|
if (isEqual) {
|
|
1184
1201
|
if (state == ConnTypes.DISCONNECTED) {
|
|
@@ -1692,6 +1709,7 @@ class MentraLive : SGCManager() {
|
|
|
1692
1709
|
}
|
|
1693
1710
|
val token = gattTeardownBarrier.beginTeardown()
|
|
1694
1711
|
gattTeardownToken = token
|
|
1712
|
+
closeEvidenceConnection("teardown")
|
|
1695
1713
|
// Teardown owns session invalidation, including the timeout/replacement paths that do
|
|
1696
1714
|
// not receive the normal remote-disconnect cleanup callback.
|
|
1697
1715
|
cancelMtuWatchdog()
|
|
@@ -1765,6 +1783,34 @@ class MentraLive : SGCManager() {
|
|
|
1765
1783
|
mtuSetupGate.cancel()
|
|
1766
1784
|
}
|
|
1767
1785
|
|
|
1786
|
+
private fun closeEvidenceConnection(reason: String) {
|
|
1787
|
+
evidenceOrigin?.let { BleEvidenceLog.connectionClosed(it.connection, reason) }
|
|
1788
|
+
evidenceOrigin = null
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/** RX/TX carry every JSON/K900 message; audio and file characteristics are never evidence. */
|
|
1792
|
+
private fun isCommandCharacteristic(characteristic: BluetoothGattCharacteristic): Boolean =
|
|
1793
|
+
characteristic.uuid == RX_CHAR_UUID || characteristic.uuid == TX_CHAR_UUID
|
|
1794
|
+
|
|
1795
|
+
private fun recordScanSend(trace: BleWriteTrace?, outcome: String) {
|
|
1796
|
+
val scanId = trace?.scanId ?: return
|
|
1797
|
+
BleEvidenceLog.scanSend(
|
|
1798
|
+
scanId,
|
|
1799
|
+
outcome,
|
|
1800
|
+
trace.chunkIndex ?: 0,
|
|
1801
|
+
trace.totalChunks ?: 1,
|
|
1802
|
+
trace.evidenceConnection,
|
|
1803
|
+
trace.evidenceWriteGeneration
|
|
1804
|
+
)
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/** Records the send-queue generation boundary that queued writes will reference. */
|
|
1808
|
+
private fun recordWriteGeneration(generation: Long) {
|
|
1809
|
+
evidenceWriteGeneration =
|
|
1810
|
+
BleEvidenceLog.bleGeneration(evidenceOrigin?.connection ?: 0L, generation)
|
|
1811
|
+
evidenceWriteGenerationValue = generation
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1768
1814
|
/**
|
|
1769
1815
|
* Reject callbacks delivered by a GATT object from an earlier connection attempt. Without
|
|
1770
1816
|
* this identity check, a late disconnect can tear down the active connection and clear its
|
|
@@ -2276,6 +2322,15 @@ class MentraLive : SGCManager() {
|
|
|
2276
2322
|
// This lets connectGatt return and install the identity before validation.
|
|
2277
2323
|
gattLifecycleHandler.post { work() }
|
|
2278
2324
|
}, { gatt -> epoch == gattEpoch && isCurrentGattCallback(gatt) }) {
|
|
2325
|
+
override fun onStaleNotification(
|
|
2326
|
+
gatt: BluetoothGatt,
|
|
2327
|
+
characteristic: BluetoothGattCharacteristic
|
|
2328
|
+
) {
|
|
2329
|
+
if (isCommandCharacteristic(characteristic)) {
|
|
2330
|
+
BleEvidenceLog.receiveRejected("stale_gatt", null)
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2279
2334
|
override fun handleConnectionStateChange(
|
|
2280
2335
|
gatt: BluetoothGatt,
|
|
2281
2336
|
status: Int,
|
|
@@ -2313,6 +2368,8 @@ class MentraLive : SGCManager() {
|
|
|
2313
2368
|
isConnecting = false
|
|
2314
2369
|
isConnected = true
|
|
2315
2370
|
connectedDevice = gatt.device
|
|
2371
|
+
closeEvidenceConnection("replaced")
|
|
2372
|
+
evidenceOrigin = BleEvidenceLog.connectionAccepted(gatt.device.address)
|
|
2316
2373
|
classicAudioConnectionTracker.setTarget(gatt.device.address)
|
|
2317
2374
|
refreshClassicAudioConnectionState(gatt.device)
|
|
2318
2375
|
emitConnectedPendingDeviceForPairingScan()
|
|
@@ -2384,6 +2441,7 @@ class MentraLive : SGCManager() {
|
|
|
2384
2441
|
)
|
|
2385
2442
|
isConnected = false
|
|
2386
2443
|
isConnecting = false
|
|
2444
|
+
closeEvidenceConnection("remote_disconnect")
|
|
2387
2445
|
|
|
2388
2446
|
connectedDevice = null
|
|
2389
2447
|
glassesReady = false // Reset ready state on disconnect
|
|
@@ -2700,6 +2758,10 @@ class MentraLive : SGCManager() {
|
|
|
2700
2758
|
else null
|
|
2701
2759
|
inFlightBleWriteTrace = null
|
|
2702
2760
|
inFlightBleWriteStartedAtMs = 0L
|
|
2761
|
+
recordScanSend(
|
|
2762
|
+
trace,
|
|
2763
|
+
if (status == BluetoothGatt.GATT_SUCCESS) "write_ok" else "write_failed"
|
|
2764
|
+
)
|
|
2703
2765
|
|
|
2704
2766
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2705
2767
|
// Bridge.log("LIVE: Characteristic write successful");
|
|
@@ -2759,8 +2821,13 @@ class MentraLive : SGCManager() {
|
|
|
2759
2821
|
data: ByteArray
|
|
2760
2822
|
) {
|
|
2761
2823
|
if (!isActiveGattCallback(gatt)) {
|
|
2824
|
+
if (isCommandCharacteristic(characteristic)) {
|
|
2825
|
+
BleEvidenceLog.receiveRejected("teardown", null)
|
|
2826
|
+
}
|
|
2762
2827
|
return
|
|
2763
2828
|
}
|
|
2829
|
+
// Immutable origin of this notification, captured before any decoding.
|
|
2830
|
+
val origin = evidenceOrigin
|
|
2764
2831
|
|
|
2765
2832
|
// Get thread ID for tracking thread issues
|
|
2766
2833
|
val threadId = Thread.currentThread().id
|
|
@@ -2807,7 +2874,7 @@ class MentraLive : SGCManager() {
|
|
|
2807
2874
|
}
|
|
2808
2875
|
|
|
2809
2876
|
// Process command/JSON data on RX/TX characteristics
|
|
2810
|
-
processReceivedData(data, data.size)
|
|
2877
|
+
processReceivedData(data, data.size, origin)
|
|
2811
2878
|
}
|
|
2812
2879
|
|
|
2813
2880
|
override fun handleDescriptorWrite(
|
|
@@ -3223,6 +3290,7 @@ class MentraLive : SGCManager() {
|
|
|
3223
3290
|
var queuedWrite = sendQueue.poll()
|
|
3224
3291
|
val currentGeneration = bleSessionGeneration.get()
|
|
3225
3292
|
while (queuedWrite != null && queuedWrite.sessionGeneration != currentGeneration) {
|
|
3293
|
+
recordScanSend(queuedWrite.trace, "stale_dropped")
|
|
3226
3294
|
Bridge.log(
|
|
3227
3295
|
"LIVE: Dropping stale queued write from BLE session " +
|
|
3228
3296
|
queuedWrite.sessionGeneration +
|
|
@@ -3258,10 +3326,15 @@ class MentraLive : SGCManager() {
|
|
|
3258
3326
|
|
|
3259
3327
|
/** Send data through BLE */
|
|
3260
3328
|
private fun sendDataInternal(write: QueuedBleWrite?) {
|
|
3261
|
-
if (
|
|
3329
|
+
if (write == null) {
|
|
3330
|
+
return
|
|
3331
|
+
}
|
|
3332
|
+
if (!isConnected) {
|
|
3333
|
+
recordScanSend(write.trace, "gatt_refused")
|
|
3262
3334
|
return
|
|
3263
3335
|
}
|
|
3264
3336
|
if (write.sessionGeneration != bleSessionGeneration.get()) {
|
|
3337
|
+
recordScanSend(write.trace, "stale_dropped")
|
|
3265
3338
|
Bridge.log("LIVE: Dropping stale BLE write before GATT transmission")
|
|
3266
3339
|
return
|
|
3267
3340
|
}
|
|
@@ -3269,6 +3342,7 @@ class MentraLive : SGCManager() {
|
|
|
3269
3342
|
val gatt = bluetoothGatt
|
|
3270
3343
|
val characteristic = txCharacteristic
|
|
3271
3344
|
if (gatt == null || characteristic == null) {
|
|
3345
|
+
recordScanSend(write.trace, "gatt_refused")
|
|
3272
3346
|
return
|
|
3273
3347
|
}
|
|
3274
3348
|
|
|
@@ -3278,6 +3352,7 @@ class MentraLive : SGCManager() {
|
|
|
3278
3352
|
inFlightBleWriteTrace = write.trace
|
|
3279
3353
|
inFlightBleWriteStartedAtMs = writeStartedAtMs
|
|
3280
3354
|
val writeAccepted = gatt.writeCharacteristic(characteristic)
|
|
3355
|
+
recordScanSend(write.trace, if (writeAccepted) "gatt_accepted" else "gatt_refused")
|
|
3281
3356
|
logBleWriteTrace(
|
|
3282
3357
|
"write_call",
|
|
3283
3358
|
write.trace,
|
|
@@ -3294,6 +3369,7 @@ class MentraLive : SGCManager() {
|
|
|
3294
3369
|
inFlightBleWriteStartedAtMs = 0L
|
|
3295
3370
|
}
|
|
3296
3371
|
} catch (e: Exception) {
|
|
3372
|
+
recordScanSend(write.trace, "gatt_refused")
|
|
3297
3373
|
Log.e(TAG, "Error sending data via BLE", e)
|
|
3298
3374
|
logBleWriteTrace(
|
|
3299
3375
|
"write_exception",
|
|
@@ -3320,9 +3396,16 @@ class MentraLive : SGCManager() {
|
|
|
3320
3396
|
queuedAtMs =
|
|
3321
3397
|
if (trace.queuedAtMs > 0L)
|
|
3322
3398
|
trace.queuedAtMs
|
|
3323
|
-
else System.currentTimeMillis()
|
|
3399
|
+
else System.currentTimeMillis(),
|
|
3400
|
+
evidenceConnection = evidenceOrigin?.connection ?: 0L,
|
|
3401
|
+
// A write carrying an older generation is already stale: no boundary.
|
|
3402
|
+
evidenceWriteGeneration =
|
|
3403
|
+
if (sessionGeneration == evidenceWriteGenerationValue)
|
|
3404
|
+
evidenceWriteGeneration
|
|
3405
|
+
else null
|
|
3324
3406
|
)
|
|
3325
3407
|
sendQueue.add(QueuedBleWrite(data, queuedTrace, sessionGeneration))
|
|
3408
|
+
recordScanSend(queuedTrace, "queued")
|
|
3326
3409
|
logBleChunkTrace(
|
|
3327
3410
|
"queued",
|
|
3328
3411
|
queuedTrace,
|
|
@@ -3543,6 +3626,7 @@ class MentraLive : SGCManager() {
|
|
|
3543
3626
|
*/
|
|
3544
3627
|
private fun resetPendingAckState() {
|
|
3545
3628
|
val nextGeneration = bleSessionGeneration.incrementAndGet()
|
|
3629
|
+
recordWriteGeneration(nextGeneration)
|
|
3546
3630
|
val pendingCount = pendingMessages.size
|
|
3547
3631
|
pendingMessages.clear()
|
|
3548
3632
|
if (pendingCount > 0) {
|
|
@@ -3871,7 +3955,7 @@ class MentraLive : SGCManager() {
|
|
|
3871
3955
|
}
|
|
3872
3956
|
|
|
3873
3957
|
/** Process data received from the glasses */
|
|
3874
|
-
private fun processReceivedData(data: ByteArray?, size: Int) {
|
|
3958
|
+
private fun processReceivedData(data: ByteArray?, size: Int, origin: BleEvidenceLog.Origin?) {
|
|
3875
3959
|
// Bridge.log("LIVE: Processing received data: " + bytesToHex(data));
|
|
3876
3960
|
|
|
3877
3961
|
// Check if we have enough data
|
|
@@ -3938,7 +4022,7 @@ class MentraLive : SGCManager() {
|
|
|
3938
4022
|
}
|
|
3939
4023
|
|
|
3940
4024
|
if (cmdType == K900ProtocolUtils.CMD_TYPE_BINARY_MSG) {
|
|
3941
|
-
processBinaryWireFrame(data)
|
|
4025
|
+
processBinaryWireFrame(data, origin)
|
|
3942
4026
|
return
|
|
3943
4027
|
}
|
|
3944
4028
|
|
|
@@ -3956,7 +4040,7 @@ class MentraLive : SGCManager() {
|
|
|
3956
4040
|
Log.w(TAG, "Thread-$threadId: Rejected unsupported compact wire form")
|
|
3957
4041
|
return
|
|
3958
4042
|
}
|
|
3959
|
-
processJsonMessage(expanded)
|
|
4043
|
+
processJsonMessage(expanded, origin)
|
|
3960
4044
|
} else {
|
|
3961
4045
|
Log.w(TAG, "Thread-" + threadId + ": Failed to parse K900 protocol data")
|
|
3962
4046
|
}
|
|
@@ -3981,7 +4065,7 @@ class MentraLive : SGCManager() {
|
|
|
3981
4065
|
val jsonStr = String(data, 0, size, StandardCharsets.UTF_8)
|
|
3982
4066
|
if (jsonStr.startsWith("{") && jsonStr.endsWith("}")) {
|
|
3983
4067
|
val json = JSONObject(jsonStr)
|
|
3984
|
-
processJsonMessage(json)
|
|
4068
|
+
processJsonMessage(json, origin)
|
|
3985
4069
|
} else {
|
|
3986
4070
|
Log.w(TAG, "Received data that starts with '{' but is not valid JSON")
|
|
3987
4071
|
}
|
|
@@ -4004,7 +4088,7 @@ class MentraLive : SGCManager() {
|
|
|
4004
4088
|
}
|
|
4005
4089
|
|
|
4006
4090
|
/** Process a JSON message */
|
|
4007
|
-
private fun processJsonMessage(json: JSONObject) {
|
|
4091
|
+
private fun processJsonMessage(json: JSONObject, origin: BleEvidenceLog.Origin?) {
|
|
4008
4092
|
// Demoted from INFO (Bridge.log) to DEBUG: per-type handlers below already log
|
|
4009
4093
|
// the messages that matter, and full payloads can leak PII (wifi SSID, bt_mac,
|
|
4010
4094
|
// OTA URLs) into the persisted file logger when they arrive every ~50ms during OTA.
|
|
@@ -4016,7 +4100,7 @@ class MentraLive : SGCManager() {
|
|
|
4016
4100
|
GlassesLinkDiagnostics.recordInbound()
|
|
4017
4101
|
|
|
4018
4102
|
if (MessageChunker.isChunkedMessage(json)) {
|
|
4019
|
-
processChunkedJsonMessage(json)
|
|
4103
|
+
processChunkedJsonMessage(json, origin)
|
|
4020
4104
|
return
|
|
4021
4105
|
}
|
|
4022
4106
|
|
|
@@ -4035,7 +4119,7 @@ class MentraLive : SGCManager() {
|
|
|
4035
4119
|
|
|
4036
4120
|
// Check if this is a K900 command format (has "C" field instead of "type")
|
|
4037
4121
|
if (json.has("C")) {
|
|
4038
|
-
processK900JsonMessage(json)
|
|
4122
|
+
processK900JsonMessage(json, origin)
|
|
4039
4123
|
return
|
|
4040
4124
|
}
|
|
4041
4125
|
|
|
@@ -4131,7 +4215,15 @@ class MentraLive : SGCManager() {
|
|
|
4131
4215
|
// that reads "charging" for most of a discharging pack's range. Charging
|
|
4132
4216
|
// state comes exclusively from the PMU charg bit in the sr_hrt heartbeat.
|
|
4133
4217
|
val percent = json.optInt("percent", batteryLevel)
|
|
4134
|
-
|
|
4218
|
+
// A missing or invalid field keeps the previous fallback display value, but only a
|
|
4219
|
+
// measured percentage carries provenance.
|
|
4220
|
+
updateBatteryStatus(
|
|
4221
|
+
percent,
|
|
4222
|
+
isCharging,
|
|
4223
|
+
measuredPercent(json, "percent")?.let {
|
|
4224
|
+
BatteryProvenance("battery_status", origin, null)
|
|
4225
|
+
}
|
|
4226
|
+
)
|
|
4135
4227
|
}
|
|
4136
4228
|
"stream_controller_probe" -> {
|
|
4137
4229
|
val values = mapOf("protocolVersion" to json.opt("protocolVersion"),
|
|
@@ -4306,6 +4398,8 @@ class MentraLive : SGCManager() {
|
|
|
4306
4398
|
"wifi_scan_result" -> {
|
|
4307
4399
|
// Process WiFi scan results
|
|
4308
4400
|
val networks: MutableList<Map<String, Any>> = ArrayList()
|
|
4401
|
+
// Evidence only: false when the network list is missing or not fully parsed.
|
|
4402
|
+
var networksParsed = false
|
|
4309
4403
|
|
|
4310
4404
|
if (json.has("networks_neo")) {
|
|
4311
4405
|
try {
|
|
@@ -4323,6 +4417,7 @@ class MentraLive : SGCManager() {
|
|
|
4323
4417
|
}
|
|
4324
4418
|
networks.add(networkMap)
|
|
4325
4419
|
}
|
|
4420
|
+
networksParsed = true
|
|
4326
4421
|
|
|
4327
4422
|
Bridge.log(
|
|
4328
4423
|
"Received enhanced WiFi scan results: " +
|
|
@@ -4337,7 +4432,9 @@ class MentraLive : SGCManager() {
|
|
|
4337
4432
|
val scanComplete =
|
|
4338
4433
|
json.optBoolean("scan_complete", json.optBoolean("scanComplete", false))
|
|
4339
4434
|
val scanId = json.optString("scanId", "").ifEmpty { null }
|
|
4340
|
-
|
|
4435
|
+
val eventId =
|
|
4436
|
+
BleEvidenceLog.scanChunk(origin, scanId, networks, scanComplete, networksParsed)
|
|
4437
|
+
Bridge.updateWifiScanResults(networks, scanComplete, scanId, eventId)
|
|
4341
4438
|
}
|
|
4342
4439
|
"token_status" -> {
|
|
4343
4440
|
// Process coreToken acknowledgment
|
|
@@ -5211,7 +5308,7 @@ class MentraLive : SGCManager() {
|
|
|
5211
5308
|
return value
|
|
5212
5309
|
}
|
|
5213
5310
|
|
|
5214
|
-
private fun processChunkedJsonMessage(json: JSONObject) {
|
|
5311
|
+
private fun processChunkedJsonMessage(json: JSONObject, origin: BleEvidenceLog.Origin?) {
|
|
5215
5312
|
try {
|
|
5216
5313
|
val chunkInfo = MessageChunker.getChunkInfo(json)
|
|
5217
5314
|
if (chunkInfo == null) {
|
|
@@ -5229,19 +5326,24 @@ class MentraLive : SGCManager() {
|
|
|
5229
5326
|
return
|
|
5230
5327
|
}
|
|
5231
5328
|
|
|
5329
|
+
val mixedBefore = incomingChunkReassembler.mixedOriginDrops
|
|
5232
5330
|
val reassembled =
|
|
5233
5331
|
incomingChunkReassembler.addChunk(
|
|
5234
5332
|
chunkInfo.chunkId,
|
|
5235
5333
|
chunkInfo.chunkIndex,
|
|
5236
5334
|
chunkInfo.totalChunks,
|
|
5237
|
-
chunkInfo.data
|
|
5335
|
+
chunkInfo.data,
|
|
5336
|
+
origin
|
|
5238
5337
|
)
|
|
5338
|
+
if (incomingChunkReassembler.mixedOriginDrops != mixedBefore) {
|
|
5339
|
+
BleEvidenceLog.receiveRejected("mixed_origin", origin)
|
|
5340
|
+
}
|
|
5239
5341
|
if (reassembled == null) {
|
|
5240
5342
|
return
|
|
5241
5343
|
}
|
|
5242
5344
|
|
|
5243
5345
|
val reassembledJson = JSONObject(reassembled)
|
|
5244
|
-
processJsonMessage(reassembledJson)
|
|
5346
|
+
processJsonMessage(reassembledJson, origin)
|
|
5245
5347
|
} catch (e: Exception) {
|
|
5246
5348
|
Log.e(TAG, "Error processing chunked JSON message", e)
|
|
5247
5349
|
}
|
|
@@ -5449,7 +5551,7 @@ class MentraLive : SGCManager() {
|
|
|
5449
5551
|
}
|
|
5450
5552
|
}
|
|
5451
5553
|
|
|
5452
|
-
private fun processK900JsonMessage(json: JSONObject) {
|
|
5554
|
+
private fun processK900JsonMessage(json: JSONObject, origin: BleEvidenceLog.Origin?) {
|
|
5453
5555
|
val command = json.optString("C", "")
|
|
5454
5556
|
// Bridge.log("LIVE: Processing K900 command: " + command);
|
|
5455
5557
|
|
|
@@ -5493,7 +5595,17 @@ class MentraLive : SGCManager() {
|
|
|
5493
5595
|
}
|
|
5494
5596
|
val charg = bodyObj.optInt("charg", -1)
|
|
5495
5597
|
if (batteryPercentage != -1 && charg != -1)
|
|
5496
|
-
updateBatteryStatus(
|
|
5598
|
+
updateBatteryStatus(
|
|
5599
|
+
batteryPercentage,
|
|
5600
|
+
charg == 1,
|
|
5601
|
+
measuredPercent(bodyObj, "pt")?.let {
|
|
5602
|
+
BatteryProvenance(
|
|
5603
|
+
"k900_sr_hrt",
|
|
5604
|
+
origin,
|
|
5605
|
+
pmuChargingBit(bodyObj)
|
|
5606
|
+
)
|
|
5607
|
+
}
|
|
5608
|
+
)
|
|
5497
5609
|
}
|
|
5498
5610
|
} catch (e: Exception) {
|
|
5499
5611
|
Log.e(TAG, "Error parsing sr_hrt response", e)
|
|
@@ -5524,7 +5636,13 @@ class MentraLive : SGCManager() {
|
|
|
5524
5636
|
// charging from voltage (>4.0V) reads "not charging" for most of a
|
|
5525
5637
|
// genuinely-charging pack's range. Charging state comes exclusively
|
|
5526
5638
|
// from the PMU charg bit in the sr_hrt heartbeat.
|
|
5527
|
-
updateBatteryStatus(
|
|
5639
|
+
updateBatteryStatus(
|
|
5640
|
+
batteryPercentage,
|
|
5641
|
+
isCharging,
|
|
5642
|
+
measuredPercent(bodyObj, "pt")?.let {
|
|
5643
|
+
BatteryProvenance("k900_sr_batv", origin, null)
|
|
5644
|
+
}
|
|
5645
|
+
)
|
|
5528
5646
|
}
|
|
5529
5647
|
} catch (e: Exception) {
|
|
5530
5648
|
Log.e(TAG, "Error parsing sr_batv response", e)
|
|
@@ -5761,7 +5879,7 @@ class MentraLive : SGCManager() {
|
|
|
5761
5879
|
Log.d(TAG, "🔓 Unwrapping and processing through standard message handler")
|
|
5762
5880
|
|
|
5763
5881
|
// Process through the standard message handler
|
|
5764
|
-
processJsonMessage(innerJson)
|
|
5882
|
+
processJsonMessage(innerJson, origin)
|
|
5765
5883
|
return // Exit after processing
|
|
5766
5884
|
}
|
|
5767
5885
|
} catch (e: JSONException) {
|
|
@@ -5875,7 +5993,44 @@ class MentraLive : SGCManager() {
|
|
|
5875
5993
|
* Update battery status and notify listeners Matches iOS MentraLive.swift updateBatteryStatus
|
|
5876
5994
|
* pattern
|
|
5877
5995
|
*/
|
|
5878
|
-
|
|
5996
|
+
/**
|
|
5997
|
+
* [source] names the decoded message. [pmuCharging] is set only when the PMU charg bit of
|
|
5998
|
+
* this very message supplied [isCharging]; percent-only sources re-pass the inherited value.
|
|
5999
|
+
*/
|
|
6000
|
+
/**
|
|
6001
|
+
* Provenance of a battery update decoded from one notification. [pmuCharging] is set only
|
|
6002
|
+
* when the PMU charg bit of that message supplied the charging state.
|
|
6003
|
+
*/
|
|
6004
|
+
private data class BatteryProvenance(
|
|
6005
|
+
val source: String,
|
|
6006
|
+
val origin: BleEvidenceLog.Origin?,
|
|
6007
|
+
val pmuCharging: Boolean?
|
|
6008
|
+
)
|
|
6009
|
+
|
|
6010
|
+
/**
|
|
6011
|
+
* The sr_hrt PMU charging bit, only when `charg` is a number exactly equal to 0 or 1. The
|
|
6012
|
+
* original value is compared without truncation, so 0.5 or 1.5 is not a bit.
|
|
6013
|
+
*/
|
|
6014
|
+
private fun pmuChargingBit(body: JSONObject): Boolean? =
|
|
6015
|
+
when ((body.opt("charg") as? Number)?.toDouble()) {
|
|
6016
|
+
1.0 -> true
|
|
6017
|
+
0.0 -> false
|
|
6018
|
+
else -> null
|
|
6019
|
+
}
|
|
6020
|
+
|
|
6021
|
+
/** The field's value when it is an actual integer percentage, else null (missing/invalid). */
|
|
6022
|
+
private fun measuredPercent(obj: JSONObject, key: String): Int? {
|
|
6023
|
+
val value = obj.opt(key) as? Number ?: return null
|
|
6024
|
+
val asDouble = value.toDouble()
|
|
6025
|
+
if (asDouble != Math.floor(asDouble) || asDouble < 0 || asDouble > 100) return null
|
|
6026
|
+
return asDouble.toInt()
|
|
6027
|
+
}
|
|
6028
|
+
|
|
6029
|
+
/**
|
|
6030
|
+
* [provenance] is null for fallback or cached values: they are still displayed as before but
|
|
6031
|
+
* carry no event id, so they can never count as a fresh measurement.
|
|
6032
|
+
*/
|
|
6033
|
+
private fun updateBatteryStatus(level: Int, isCharging: Boolean, provenance: BatteryProvenance?) {
|
|
5879
6034
|
// Keep the field in sync: percent-only messages (battery_status/sr_batv) re-pass
|
|
5880
6035
|
// it as the last-known charging state, so a stale field would clobber the value
|
|
5881
6036
|
// the sr_hrt PMU charg bit established.
|
|
@@ -5886,7 +6041,11 @@ class MentraLive : SGCManager() {
|
|
|
5886
6041
|
DeviceStore.apply("glasses", "charging", isCharging)
|
|
5887
6042
|
|
|
5888
6043
|
if (level >= 0) {
|
|
5889
|
-
|
|
6044
|
+
val eventId =
|
|
6045
|
+
provenance?.let {
|
|
6046
|
+
BleEvidenceLog.battery(it.origin, level, it.source, it.pmuCharging)
|
|
6047
|
+
}
|
|
6048
|
+
Bridge.sendBatteryStatus(level, isCharging, eventId)
|
|
5890
6049
|
}
|
|
5891
6050
|
}
|
|
5892
6051
|
|
|
@@ -6063,7 +6222,17 @@ class MentraLive : SGCManager() {
|
|
|
6063
6222
|
if (!scanId.isNullOrEmpty()) {
|
|
6064
6223
|
json.put("scanId", scanId)
|
|
6065
6224
|
}
|
|
6066
|
-
sendJson(json, true)
|
|
6225
|
+
val queued = sendJson(json, true, true)
|
|
6226
|
+
if (!queued && !scanId.isNullOrEmpty()) {
|
|
6227
|
+
BleEvidenceLog.scanSend(
|
|
6228
|
+
scanId,
|
|
6229
|
+
"not_queued",
|
|
6230
|
+
0,
|
|
6231
|
+
0,
|
|
6232
|
+
evidenceOrigin?.connection ?: 0L,
|
|
6233
|
+
evidenceWriteGeneration
|
|
6234
|
+
)
|
|
6235
|
+
}
|
|
6067
6236
|
Bridge.log("LIVE: Sending WiFi scan request to glasses")
|
|
6068
6237
|
} catch (e: JSONException) {
|
|
6069
6238
|
Log.e(TAG, "Error creating WiFi scan request", e)
|
|
@@ -9009,6 +9178,9 @@ class MentraLive : SGCManager() {
|
|
|
9009
9178
|
* the per-session handshake attempt cap) instead of trusting stale v2-active state.
|
|
9010
9179
|
*/
|
|
9011
9180
|
private fun resetWireNegotiationState() {
|
|
9181
|
+
evidenceOrigin?.let {
|
|
9182
|
+
evidenceOrigin = it.copy(wire = BleEvidenceLog.wireReset(it.connection))
|
|
9183
|
+
}
|
|
9012
9184
|
incomingChunkReassembler.clear()
|
|
9013
9185
|
peerWireProtocolVersion = 0
|
|
9014
9186
|
useBinaryWireProtocol = false
|
|
@@ -9244,7 +9416,7 @@ class MentraLive : SGCManager() {
|
|
|
9244
9416
|
activateBinaryWireV2Session("LIVE: Peer confirmed BLE wire protocol v2")
|
|
9245
9417
|
}
|
|
9246
9418
|
|
|
9247
|
-
private fun processBinaryWireFrame(data: ByteArray) {
|
|
9419
|
+
private fun processBinaryWireFrame(data: ByteArray, origin: BleEvidenceLog.Origin?) {
|
|
9248
9420
|
val info = K900ProtocolUtils.extractBinaryFragmentInfo(data) ?: run {
|
|
9249
9421
|
Log.w(TAG, "Failed to parse binary wire frame")
|
|
9250
9422
|
return
|
|
@@ -9261,14 +9433,19 @@ class MentraLive : SGCManager() {
|
|
|
9261
9433
|
)
|
|
9262
9434
|
}
|
|
9263
9435
|
|
|
9436
|
+
val mixedBefore = incomingChunkReassembler.mixedOriginDrops
|
|
9264
9437
|
val reassembled =
|
|
9265
9438
|
incomingChunkReassembler.addBinaryFragment(
|
|
9266
9439
|
info.msgId,
|
|
9267
9440
|
info.fragIdx,
|
|
9268
9441
|
info.fragCount,
|
|
9269
|
-
info.payload
|
|
9442
|
+
info.payload,
|
|
9443
|
+
origin
|
|
9270
9444
|
)
|
|
9271
|
-
|
|
9445
|
+
if (incomingChunkReassembler.mixedOriginDrops != mixedBefore) {
|
|
9446
|
+
BleEvidenceLog.receiveRejected("mixed_origin", origin)
|
|
9447
|
+
}
|
|
9448
|
+
if (reassembled == null) return
|
|
9272
9449
|
|
|
9273
9450
|
try {
|
|
9274
9451
|
val jsonStr = String(reassembled, StandardCharsets.UTF_8)
|
|
@@ -9284,7 +9461,7 @@ class MentraLive : SGCManager() {
|
|
|
9284
9461
|
BleWireProtocol.PROTOCOL_V2,
|
|
9285
9462
|
"glasses_to_phone"
|
|
9286
9463
|
)
|
|
9287
|
-
processJsonMessage(json)
|
|
9464
|
+
processJsonMessage(json, origin)
|
|
9288
9465
|
} catch (e: JSONException) {
|
|
9289
9466
|
Log.e(TAG, "Failed to parse reassembled binary wire JSON", e)
|
|
9290
9467
|
}
|
|
@@ -9493,7 +9670,11 @@ class MentraLive : SGCManager() {
|
|
|
9493
9670
|
}
|
|
9494
9671
|
} else {
|
|
9495
9672
|
// Normal single message transmission
|
|
9496
|
-
transportLog(
|
|
9673
|
+
transportLog(
|
|
9674
|
+
"LIVE: Sending data to glasses: " +
|
|
9675
|
+
loggableOutgoingPayload(wireData, commandTraceInfo.commandType),
|
|
9676
|
+
bridgeLogging
|
|
9677
|
+
)
|
|
9497
9678
|
|
|
9498
9679
|
// Pack the data using the centralized utility with the negotiated endianness
|
|
9499
9680
|
val packedData =
|
|
@@ -9625,11 +9806,15 @@ class MentraLive : SGCManager() {
|
|
|
9625
9806
|
private fun parseOutgoingBleCommandTraceInfo(payload: String): OutgoingBleCommandTraceInfo {
|
|
9626
9807
|
return try {
|
|
9627
9808
|
val obj = JSONObject(payload)
|
|
9809
|
+
val commandType = obj.optString("type", "unknown")
|
|
9628
9810
|
OutgoingBleCommandTraceInfo(
|
|
9629
|
-
commandType =
|
|
9811
|
+
commandType = commandType,
|
|
9630
9812
|
requestId = optNonBlankString(obj, "requestId"),
|
|
9631
9813
|
appId = optNonBlankString(obj, "appId"),
|
|
9632
|
-
messageId = if (obj.has("mId")) obj.optLong("mId") else null
|
|
9814
|
+
messageId = if (obj.has("mId")) obj.optLong("mId") else null,
|
|
9815
|
+
scanId =
|
|
9816
|
+
if (commandType == "request_wifi_scan") optNonBlankString(obj, "scanId")
|
|
9817
|
+
else null
|
|
9633
9818
|
)
|
|
9634
9819
|
} catch (_: Exception) {
|
|
9635
9820
|
OutgoingBleCommandTraceInfo(
|
|
@@ -9664,7 +9849,8 @@ class MentraLive : SGCManager() {
|
|
|
9664
9849
|
packedBytes = packedBytes,
|
|
9665
9850
|
wakeup = wakeup,
|
|
9666
9851
|
chunked = chunked,
|
|
9667
|
-
queuedAtMs = System.currentTimeMillis()
|
|
9852
|
+
queuedAtMs = System.currentTimeMillis(),
|
|
9853
|
+
scanId = commandInfo.scanId
|
|
9668
9854
|
)
|
|
9669
9855
|
}
|
|
9670
9856
|
|
|
@@ -9795,6 +9981,18 @@ class MentraLive : SGCManager() {
|
|
|
9795
9981
|
}
|
|
9796
9982
|
}
|
|
9797
9983
|
|
|
9984
|
+
/**
|
|
9985
|
+
* Wi-Fi credentials are sent unchanged but never logged; the BLE trace records the command
|
|
9986
|
+
* with the password redacted.
|
|
9987
|
+
*/
|
|
9988
|
+
private fun loggableOutgoingPayload(payload: String, commandType: String): String =
|
|
9989
|
+
try {
|
|
9990
|
+
if (JSONObject(payload).has("password")) "<$commandType with credentials omitted>"
|
|
9991
|
+
else payload
|
|
9992
|
+
} catch (_: JSONException) {
|
|
9993
|
+
payload
|
|
9994
|
+
}
|
|
9995
|
+
|
|
9798
9996
|
private fun summarizeOutgoingMessage(payload: String?): String {
|
|
9799
9997
|
if (payload == null || payload.isEmpty()) {
|
|
9800
9998
|
return "type=unknown, requestId=none, appId=none, mode=none, transferMethod=none, bleImgId=none, exposureTimeNs=none, iso=none, mId=none"
|
|
@@ -18,6 +18,17 @@ internal abstract class SerializedGattCallback(
|
|
|
18
18
|
enqueue { if (isCurrent(gatt)) work() }
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
private fun dispatchNotification(
|
|
22
|
+
gatt: BluetoothGatt,
|
|
23
|
+
characteristic: BluetoothGattCharacteristic,
|
|
24
|
+
work: () -> Unit,
|
|
25
|
+
) {
|
|
26
|
+
enqueue { if (isCurrent(gatt)) work() else onStaleNotification(gatt, characteristic) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A notification from a GATT that is no longer current was dropped on the owner queue. */
|
|
30
|
+
protected open fun onStaleNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {}
|
|
31
|
+
|
|
21
32
|
final override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) =
|
|
22
33
|
dispatch(gatt) { handleConnectionStateChange(gatt, status, newState) }
|
|
23
34
|
|
|
@@ -46,14 +57,14 @@ internal abstract class SerializedGattCallback(
|
|
|
46
57
|
gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic,
|
|
47
58
|
) {
|
|
48
59
|
val value = characteristic.value?.copyOf() ?: return
|
|
49
|
-
|
|
60
|
+
dispatchNotification(gatt, characteristic) { handleCharacteristicChanged(gatt, characteristic, value) }
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
final override fun onCharacteristicChanged(
|
|
53
64
|
gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray,
|
|
54
65
|
) {
|
|
55
66
|
val snapshot = value.copyOf()
|
|
56
|
-
|
|
67
|
+
dispatchNotification(gatt, characteristic) { handleCharacteristicChanged(gatt, characteristic, snapshot) }
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
final override fun onDescriptorWrite(
|