@mentra/bluetooth-sdk 3.2.0-dev.195 → 3.2.0-dev.203
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 +64 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +77 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +12 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +318 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +88 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +50 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +626 -120
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattLifecycle.kt +64 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +76 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +16 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoBridgeTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +169 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceManifestTest.kt +57 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +52 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +94 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattLifecycleTest.kt +112 -0
- package/build/BluetoothSdk.types.d.ts +62 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +3 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +8 -2
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +78 -1
- package/ios/Source/DeviceManager.swift +13 -6
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +397 -99
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +101 -0
- package/ios/Source/sgcs/MentraLive.swift +179 -37
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +30 -3
- package/ios/Tests/VersionInfoBridgeTests.swift +32 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +181 -0
- package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +76 -1
- package/src/_private/BluetoothSdkModule.ts +4 -1
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +11 -0
|
@@ -39,6 +39,8 @@ import com.mentra.bluetoothsdk.PhotoSize
|
|
|
39
39
|
import com.mentra.bluetoothsdk.PhotoMode
|
|
40
40
|
import com.mentra.bluetoothsdk.DeviceStore
|
|
41
41
|
import com.mentra.bluetoothsdk.ObservableStore
|
|
42
|
+
import com.mentra.bluetoothsdk.incomingGlassesMessageAckId
|
|
43
|
+
import com.mentra.bluetoothsdk.wifiResponseEnvelopeIsValid
|
|
42
44
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
43
45
|
import com.mentra.bluetoothsdk.utils.BlePhotoUploadService
|
|
44
46
|
import com.mentra.bluetoothsdk.utils.ConnTypes
|
|
@@ -141,6 +143,9 @@ class MentraLive : SGCManager() {
|
|
|
141
143
|
private const val A2DP_CONNECT_MAX_ATTEMPTS = 5
|
|
142
144
|
private const val A2DP_CONNECT_RETRY_MS = 800L
|
|
143
145
|
private const val UNPAIR_FLUSH_MS = 400L
|
|
146
|
+
private const val GATT_DISCONNECT_TIMEOUT_MS = 2_000L
|
|
147
|
+
private const val MTU_CALLBACK_TIMEOUT_MS = 3_000L
|
|
148
|
+
private val gattTeardownBarrier = MentraLiveGattTeardownBarrier()
|
|
144
149
|
|
|
145
150
|
// L2CAP CoC fast path: new BES2700 firmware registers an LE L2CAP CoC server on this
|
|
146
151
|
// PSM. When the phone opens the channel, the glasses send file packets over it instead
|
|
@@ -564,6 +569,12 @@ class MentraLive : SGCManager() {
|
|
|
564
569
|
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
565
570
|
private var bluetoothScanner: BluetoothLeScanner? = null
|
|
566
571
|
@Volatile private var bluetoothGatt: BluetoothGatt? = null
|
|
572
|
+
private val gattLifecycleHandler = Handler(Looper.getMainLooper())
|
|
573
|
+
// Owned exclusively by the main looper, including callback validation and mutation.
|
|
574
|
+
private var gattEpoch = 0L
|
|
575
|
+
private var connectionRequestEpoch = 0L
|
|
576
|
+
@Volatile private var gattTeardownToken: Long? = null
|
|
577
|
+
@Volatile private var gattTeardownTimeoutRunnable: Runnable? = null
|
|
567
578
|
private var connectedDevice: BluetoothDevice? = null
|
|
568
579
|
private var txCharacteristic: BluetoothGattCharacteristic? = null
|
|
569
580
|
private var rxCharacteristic: BluetoothGattCharacteristic? = null
|
|
@@ -591,9 +602,14 @@ class MentraLive : SGCManager() {
|
|
|
591
602
|
|
|
592
603
|
// CTKD (Cross-Transport Key Derivation) support for BES devices
|
|
593
604
|
private var isBondingReceiverRegistered = false
|
|
594
|
-
private var isBtClassicConnected = false
|
|
595
605
|
private var bondingReceiver: BroadcastReceiver? = null
|
|
596
606
|
private var bondingRetryCount = 0
|
|
607
|
+
private var classicAudioReceiver: BroadcastReceiver? = null
|
|
608
|
+
private var isClassicAudioReceiverRegistered = false
|
|
609
|
+
private val classicAudioConnectionTracker =
|
|
610
|
+
ClassicAudioConnectionTracker { connected ->
|
|
611
|
+
DeviceStore.apply("glasses", "bluetoothClassicConnected", connected)
|
|
612
|
+
}
|
|
597
613
|
|
|
598
614
|
// Pairing timing diagnostics (filter logcat: PAIRING_TIMING)
|
|
599
615
|
private var pairingTimingActive = false
|
|
@@ -643,6 +659,9 @@ class MentraLive : SGCManager() {
|
|
|
643
659
|
private var isDescriptorWriteInProgress = false
|
|
644
660
|
private var notificationsEnabled =
|
|
645
661
|
false // Track if enableNotifications was already called this connection
|
|
662
|
+
private val mtuSetupGate = MentraLiveMtuSetupGate()
|
|
663
|
+
private var mtuSetupToken: Long? = null
|
|
664
|
+
private var mtuWatchdogRunnable: Runnable? = null
|
|
646
665
|
private var connectionTimeoutRunnable: Runnable? = null
|
|
647
666
|
private var connectionTimeoutHandler = Handler(Looper.getMainLooper())
|
|
648
667
|
private var processSendQueueRunnable: Runnable? = null
|
|
@@ -992,6 +1011,8 @@ class MentraLive : SGCManager() {
|
|
|
992
1011
|
|
|
993
1012
|
// Initialize CTKD bonding receiver
|
|
994
1013
|
initializeBondingReceiver()
|
|
1014
|
+
initializeClassicAudioReceiver()
|
|
1015
|
+
registerClassicAudioReceiver()
|
|
995
1016
|
|
|
996
1017
|
// Initialize the send queue processor
|
|
997
1018
|
processSendQueueRunnable = Runnable {
|
|
@@ -1233,6 +1254,7 @@ class MentraLive : SGCManager() {
|
|
|
1233
1254
|
|
|
1234
1255
|
/** Starts BLE scanning for Mentra Live glasses */
|
|
1235
1256
|
private fun startScan() {
|
|
1257
|
+
if (postGattLifecycle { startScan() }) return
|
|
1236
1258
|
if (pairingYieldActive) {
|
|
1237
1259
|
Bridge.log("LIVE: startScan blocked — pairing yield active")
|
|
1238
1260
|
return
|
|
@@ -1297,6 +1319,7 @@ class MentraLive : SGCManager() {
|
|
|
1297
1319
|
|
|
1298
1320
|
val generation = ++scanGeneration
|
|
1299
1321
|
isScanning = true
|
|
1322
|
+
scanCallback = createScanCallback(generation)
|
|
1300
1323
|
bluetoothScanner!!.startScan(filters, settings, scanCallback)
|
|
1301
1324
|
|
|
1302
1325
|
// Set a timeout to stop scanning
|
|
@@ -1349,6 +1372,7 @@ class MentraLive : SGCManager() {
|
|
|
1349
1372
|
|
|
1350
1373
|
/** Stops BLE scanning */
|
|
1351
1374
|
override fun stopScan() {
|
|
1375
|
+
if (postGattLifecycle { stopScan() }) return
|
|
1352
1376
|
manualDiscoveryActive = false
|
|
1353
1377
|
if (bluetoothAdapter == null || bluetoothScanner == null || !isScanning) {
|
|
1354
1378
|
return
|
|
@@ -1424,9 +1448,13 @@ class MentraLive : SGCManager() {
|
|
|
1424
1448
|
var seenDevices: MutableSet<String> = HashSet()
|
|
1425
1449
|
|
|
1426
1450
|
/** BLE Scan callback */
|
|
1427
|
-
private
|
|
1451
|
+
private var scanCallback: ScanCallback? = null
|
|
1452
|
+
|
|
1453
|
+
private fun createScanCallback(generation: Int): ScanCallback =
|
|
1428
1454
|
object : ScanCallback() {
|
|
1429
1455
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
1456
|
+
if (postGattLifecycle { onScanResult(callbackType, result) }) return
|
|
1457
|
+
if (!isScanning || generation != scanGeneration) return
|
|
1430
1458
|
// Check if the object has been destroyed to prevent NPE
|
|
1431
1459
|
if (context == null || isKilled) {
|
|
1432
1460
|
Bridge.log("LIVE: Ignoring scan result - object destroyed or killed")
|
|
@@ -1559,6 +1587,8 @@ class MentraLive : SGCManager() {
|
|
|
1559
1587
|
}
|
|
1560
1588
|
|
|
1561
1589
|
override fun onScanFailed(errorCode: Int) {
|
|
1590
|
+
if (postGattLifecycle { onScanFailed(errorCode) }) return
|
|
1591
|
+
if (generation != scanGeneration || !isScanning) return
|
|
1562
1592
|
Log.e(TAG, "BLE scan failed with error: " + errorCode)
|
|
1563
1593
|
isScanning = false
|
|
1564
1594
|
if (isReconnecting && !isKilled) {
|
|
@@ -1588,30 +1618,120 @@ class MentraLive : SGCManager() {
|
|
|
1588
1618
|
}
|
|
1589
1619
|
}
|
|
1590
1620
|
|
|
1591
|
-
/**
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1621
|
+
/** Return true when work was handed to the single process-wide lifecycle queue. */
|
|
1622
|
+
private fun postGattLifecycle(work: () -> Unit): Boolean {
|
|
1623
|
+
if (Looper.myLooper() == gattLifecycleHandler.looper) return false
|
|
1624
|
+
gattLifecycleHandler.post { work() }
|
|
1625
|
+
return true
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/** Safely close GATT, waiting for Android to finish a requested disconnect when possible. */
|
|
1597
1629
|
private fun closeGattQuietly(disconnect: Boolean) {
|
|
1630
|
+
check(Looper.myLooper() == gattLifecycleHandler.looper)
|
|
1598
1631
|
val gatt = bluetoothGatt
|
|
1599
|
-
bluetoothGatt = null
|
|
1600
1632
|
if (gatt == null) {
|
|
1601
1633
|
return
|
|
1602
1634
|
}
|
|
1635
|
+
cancelMtuWatchdog()
|
|
1636
|
+
if (disconnect) {
|
|
1637
|
+
beginGattTeardown(gatt)
|
|
1638
|
+
return
|
|
1639
|
+
}
|
|
1640
|
+
bluetoothGatt = null
|
|
1641
|
+
releaseGattSessionMedia()
|
|
1603
1642
|
try {
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1643
|
+
gatt.close()
|
|
1644
|
+
} catch (e: Exception) {
|
|
1645
|
+
Log.w(TAG, "🔌 closeGattQuietly: close threw " + e)
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/** Release side channels on every teardown, including missing disconnect callbacks. */
|
|
1650
|
+
private fun releaseGattSessionMedia() {
|
|
1651
|
+
closeL2capFileChannel()
|
|
1652
|
+
fileProcessingHandler.removeCallbacksAndMessages(null)
|
|
1653
|
+
clearFilePacketBuffer()
|
|
1654
|
+
closeLc3Logging()
|
|
1655
|
+
lc3AudioPlayer?.stopPlay()
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
private fun beginGattTeardown(gatt: BluetoothGatt) {
|
|
1659
|
+
check(Looper.myLooper() == gattLifecycleHandler.looper)
|
|
1660
|
+
if (gatt !== bluetoothGatt || gattTeardownToken != null) {
|
|
1661
|
+
return
|
|
1662
|
+
}
|
|
1663
|
+
val token = gattTeardownBarrier.beginTeardown()
|
|
1664
|
+
gattTeardownToken = token
|
|
1665
|
+
// Teardown owns session invalidation, including the timeout/replacement paths that do
|
|
1666
|
+
// not receive the normal remote-disconnect cleanup callback.
|
|
1667
|
+
cancelMtuWatchdog()
|
|
1668
|
+
isConnected = false
|
|
1669
|
+
isConnecting = false
|
|
1670
|
+
connectedDevice = null
|
|
1671
|
+
glassesReady = false
|
|
1672
|
+
glassesSessionId = null
|
|
1673
|
+
readinessCompletedThisBleSession = false
|
|
1674
|
+
glassesReadyReceived = false
|
|
1675
|
+
ctkdInitiatedThisGattSession = false
|
|
1676
|
+
audioConnected = false
|
|
1677
|
+
notificationsEnabled = false
|
|
1678
|
+
currentMtu = 23
|
|
1679
|
+
pendingDescriptorWrites.clear()
|
|
1680
|
+
isDescriptorWriteInProgress = false
|
|
1681
|
+
txCharacteristic = null
|
|
1682
|
+
rxCharacteristic = null
|
|
1683
|
+
lc3ReadCharacteristic = null
|
|
1684
|
+
lc3WriteCharacteristic = null
|
|
1685
|
+
handler.removeCallbacks(processSendQueueRunnable!!)
|
|
1686
|
+
stopReadinessCheckLoop()
|
|
1687
|
+
stopHeartbeat()
|
|
1688
|
+
stopSignalStrengthPolling()
|
|
1689
|
+
stopMicBeat()
|
|
1690
|
+
releaseGattSessionMedia()
|
|
1691
|
+
updateConnectionState(ConnTypes.DISCONNECTED)
|
|
1692
|
+
val timeout =
|
|
1693
|
+
Runnable {
|
|
1694
|
+
Bridge.log(
|
|
1695
|
+
"LIVE: 🔌 GATT disconnect callback timed out after ${GATT_DISCONNECT_TIMEOUT_MS}ms; closing transport"
|
|
1696
|
+
)
|
|
1697
|
+
completeGattTeardown(gatt, token, "timeout")
|
|
1698
|
+
}
|
|
1699
|
+
gattTeardownTimeoutRunnable = timeout
|
|
1700
|
+
gattLifecycleHandler.postDelayed(timeout, GATT_DISCONNECT_TIMEOUT_MS)
|
|
1701
|
+
try {
|
|
1702
|
+
Bridge.log("LIVE: 🔌 Waiting for GATT disconnected callback before close")
|
|
1703
|
+
gatt.disconnect()
|
|
1607
1704
|
} catch (e: Exception) {
|
|
1608
|
-
Log.w(TAG, "🔌
|
|
1705
|
+
Log.w(TAG, "🔌 GATT disconnect threw " + e)
|
|
1706
|
+
completeGattTeardown(gatt, token, "disconnect_exception")
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
private fun completeGattTeardown(gatt: BluetoothGatt, token: Long, reason: String) {
|
|
1711
|
+
check(Looper.myLooper() == gattLifecycleHandler.looper)
|
|
1712
|
+
if (gattTeardownToken != token) {
|
|
1713
|
+
return
|
|
1714
|
+
}
|
|
1715
|
+
gattTeardownTimeoutRunnable?.let { gattLifecycleHandler.removeCallbacks(it) }
|
|
1716
|
+
gattTeardownTimeoutRunnable = null
|
|
1717
|
+
gattTeardownToken = null
|
|
1718
|
+
if (gatt === bluetoothGatt) {
|
|
1719
|
+
bluetoothGatt = null
|
|
1609
1720
|
}
|
|
1610
1721
|
try {
|
|
1611
1722
|
gatt.close()
|
|
1612
1723
|
} catch (e: Exception) {
|
|
1613
|
-
Log.w(TAG, "🔌
|
|
1724
|
+
Log.w(TAG, "🔌 GATT close after $reason threw " + e)
|
|
1614
1725
|
}
|
|
1726
|
+
Bridge.log("LIVE: 🔌 GATT teardown complete ($reason)")
|
|
1727
|
+
gattTeardownBarrier.completeTeardown(token)
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
private fun cancelMtuWatchdog() {
|
|
1731
|
+
mtuWatchdogRunnable?.let { gattLifecycleHandler.removeCallbacks(it) }
|
|
1732
|
+
mtuWatchdogRunnable = null
|
|
1733
|
+
mtuSetupToken = null
|
|
1734
|
+
mtuSetupGate.cancel()
|
|
1615
1735
|
}
|
|
1616
1736
|
|
|
1617
1737
|
/**
|
|
@@ -1634,6 +1754,11 @@ class MentraLive : SGCManager() {
|
|
|
1634
1754
|
return false
|
|
1635
1755
|
}
|
|
1636
1756
|
|
|
1757
|
+
/** Ignore all non-disconnect work once this GATT is stale or tearing down. */
|
|
1758
|
+
private fun isActiveGattCallback(gatt: BluetoothGatt): Boolean {
|
|
1759
|
+
return isCurrentGattCallback(gatt) && gattTeardownToken == null
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1637
1762
|
private fun beginPairingTiming(reason: String) {
|
|
1638
1763
|
pairingTimingStartMs = SystemClock.elapsedRealtime()
|
|
1639
1764
|
pairingTimingLastMs = pairingTimingStartMs
|
|
@@ -1662,15 +1787,32 @@ class MentraLive : SGCManager() {
|
|
|
1662
1787
|
|
|
1663
1788
|
/** Connect to a specific BLE device */
|
|
1664
1789
|
private fun connectToDevice(device: BluetoothDevice?) {
|
|
1790
|
+
if (postGattLifecycle { connectToDevice(device) }) return
|
|
1665
1791
|
if (device == null) {
|
|
1666
1792
|
return
|
|
1667
1793
|
}
|
|
1668
|
-
if (pairingYieldActive) {
|
|
1794
|
+
if (isKilled || pairingYieldActive) {
|
|
1669
1795
|
Bridge.log("LIVE: connectToDevice blocked — pairing yield active")
|
|
1670
1796
|
isConnecting = false
|
|
1671
1797
|
isReconnecting = false
|
|
1672
1798
|
return
|
|
1673
1799
|
}
|
|
1800
|
+
val requestEpoch = ++connectionRequestEpoch
|
|
1801
|
+
// Never replace a still-open GATT. Its disconnect/close must finish first.
|
|
1802
|
+
bluetoothGatt?.let { beginGattTeardown(it) }
|
|
1803
|
+
gattTeardownBarrier.runWhenIdle {
|
|
1804
|
+
if (requestEpoch == connectionRequestEpoch && !isKilled && !pairingYieldActive) {
|
|
1805
|
+
connectToDeviceWhenIdle(device)
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
private fun connectToDeviceWhenIdle(device: BluetoothDevice) {
|
|
1811
|
+
check(Looper.myLooper() == gattLifecycleHandler.looper)
|
|
1812
|
+
check(bluetoothGatt == null)
|
|
1813
|
+
val epoch = ++gattEpoch
|
|
1814
|
+
currentMtu = 23
|
|
1815
|
+
val gattCallback = createGattCallback(epoch)
|
|
1674
1816
|
|
|
1675
1817
|
beginPairingTiming(
|
|
1676
1818
|
"connectToDevice addr=${device.address} name=${safeDeviceName(device)} attempt=$reconnectAttempts"
|
|
@@ -1683,7 +1825,7 @@ class MentraLive : SGCManager() {
|
|
|
1683
1825
|
|
|
1684
1826
|
// Set connection timeout
|
|
1685
1827
|
connectionTimeoutRunnable = Runnable {
|
|
1686
|
-
if (isConnecting && !isConnected) {
|
|
1828
|
+
if (epoch == gattEpoch && isConnecting && !isConnected) {
|
|
1687
1829
|
Log.w(
|
|
1688
1830
|
TAG,
|
|
1689
1831
|
"🔌 ⏰ CONNECTION TIMEOUT after " +
|
|
@@ -1775,6 +1917,8 @@ class MentraLive : SGCManager() {
|
|
|
1775
1917
|
// }
|
|
1776
1918
|
|
|
1777
1919
|
private fun enterPairingYield(windowMs: Long) {
|
|
1920
|
+
if (postGattLifecycle { enterPairingYield(windowMs) }) return
|
|
1921
|
+
connectionRequestEpoch++
|
|
1778
1922
|
pairingYieldActive = true
|
|
1779
1923
|
pairingYieldAwaitingReclaim = false
|
|
1780
1924
|
isReconnecting = false
|
|
@@ -1855,6 +1999,7 @@ class MentraLive : SGCManager() {
|
|
|
1855
1999
|
|
|
1856
2000
|
/** Handle reconnection with exponential backoff */
|
|
1857
2001
|
private fun handleReconnection() {
|
|
2002
|
+
if (postGattLifecycle { handleReconnection() }) return
|
|
1858
2003
|
// Don't attempt reconnection if we've been killed/forgotten
|
|
1859
2004
|
if (isKilled) {
|
|
1860
2005
|
Bridge.log("LIVE: 🔌 RECONNECT ABORTED - device has been killed/forgotten")
|
|
@@ -2093,9 +2238,13 @@ class MentraLive : SGCManager() {
|
|
|
2093
2238
|
}
|
|
2094
2239
|
|
|
2095
2240
|
/** GATT callback for BLE operations */
|
|
2096
|
-
private
|
|
2097
|
-
object :
|
|
2098
|
-
|
|
2241
|
+
private fun createGattCallback(epoch: Long): BluetoothGattCallback =
|
|
2242
|
+
object : SerializedGattCallback({ work ->
|
|
2243
|
+
// Always enqueue, even for an inline platform callback during connectGatt().
|
|
2244
|
+
// This lets connectGatt return and install the identity before validation.
|
|
2245
|
+
gattLifecycleHandler.post { work() }
|
|
2246
|
+
}, { gatt -> epoch == gattEpoch && isCurrentGattCallback(gatt) }) {
|
|
2247
|
+
override fun handleConnectionStateChange(
|
|
2099
2248
|
gatt: BluetoothGatt,
|
|
2100
2249
|
status: Int,
|
|
2101
2250
|
newState: Int
|
|
@@ -2103,6 +2252,19 @@ class MentraLive : SGCManager() {
|
|
|
2103
2252
|
if (!isCurrentGattCallback(gatt)) {
|
|
2104
2253
|
return
|
|
2105
2254
|
}
|
|
2255
|
+
val teardownToken = gattTeardownToken
|
|
2256
|
+
if (teardownToken != null) {
|
|
2257
|
+
if (
|
|
2258
|
+
newState == BluetoothProfile.STATE_DISCONNECTED ||
|
|
2259
|
+
status != BluetoothGatt.GATT_SUCCESS
|
|
2260
|
+
) {
|
|
2261
|
+
completeGattTeardown(gatt, teardownToken, "disconnected_callback")
|
|
2262
|
+
}
|
|
2263
|
+
// Once teardown starts, a queued successful CONNECTED callback belongs to
|
|
2264
|
+
// the closing transport. It must not resurrect application connection
|
|
2265
|
+
// state while the disconnect callback is still pending.
|
|
2266
|
+
return
|
|
2267
|
+
}
|
|
2106
2268
|
|
|
2107
2269
|
// Cancel the connection timeout
|
|
2108
2270
|
if (connectionTimeoutRunnable != null) {
|
|
@@ -2119,6 +2281,8 @@ class MentraLive : SGCManager() {
|
|
|
2119
2281
|
isConnecting = false
|
|
2120
2282
|
isConnected = true
|
|
2121
2283
|
connectedDevice = gatt.device
|
|
2284
|
+
classicAudioConnectionTracker.setTarget(gatt.device.address)
|
|
2285
|
+
refreshClassicAudioConnectionState(gatt.device)
|
|
2122
2286
|
emitConnectedPendingDeviceForPairingScan()
|
|
2123
2287
|
|
|
2124
2288
|
DeviceStore.apply("glasses", "bluetoothName", connectedDevice!!.name)
|
|
@@ -2209,11 +2373,6 @@ class MentraLive : SGCManager() {
|
|
|
2209
2373
|
// Stop micbeat mechanism
|
|
2210
2374
|
stopMicBeat()
|
|
2211
2375
|
|
|
2212
|
-
// Close the L2CAP file channel (if the fast path was open)
|
|
2213
|
-
closeL2capFileChannel()
|
|
2214
|
-
fileProcessingHandler.removeCallbacksAndMessages(null)
|
|
2215
|
-
clearFilePacketBuffer()
|
|
2216
|
-
|
|
2217
2376
|
// Clean up GATT resources
|
|
2218
2377
|
closeGattQuietly(false)
|
|
2219
2378
|
|
|
@@ -2223,13 +2382,6 @@ class MentraLive : SGCManager() {
|
|
|
2223
2382
|
handleReconnection()
|
|
2224
2383
|
}
|
|
2225
2384
|
|
|
2226
|
-
// Close LC3 audio logging
|
|
2227
|
-
closeLc3Logging()
|
|
2228
|
-
|
|
2229
|
-
// stop LC3 player
|
|
2230
|
-
if (lc3AudioPlayer != null) {
|
|
2231
|
-
lc3AudioPlayer!!.stopPlay()
|
|
2232
|
-
}
|
|
2233
2385
|
}
|
|
2234
2386
|
} else {
|
|
2235
2387
|
// Connection error
|
|
@@ -2302,7 +2454,7 @@ class MentraLive : SGCManager() {
|
|
|
2302
2454
|
}
|
|
2303
2455
|
}
|
|
2304
2456
|
|
|
2305
|
-
override fun
|
|
2457
|
+
override fun handlePhyUpdate(
|
|
2306
2458
|
gatt: BluetoothGatt,
|
|
2307
2459
|
txPhy: Int,
|
|
2308
2460
|
rxPhy: Int,
|
|
@@ -2318,7 +2470,7 @@ class MentraLive : SGCManager() {
|
|
|
2318
2470
|
)
|
|
2319
2471
|
}
|
|
2320
2472
|
|
|
2321
|
-
override fun
|
|
2473
|
+
override fun handlePhyRead(
|
|
2322
2474
|
gatt: BluetoothGatt,
|
|
2323
2475
|
txPhy: Int,
|
|
2324
2476
|
rxPhy: Int,
|
|
@@ -2334,7 +2486,10 @@ class MentraLive : SGCManager() {
|
|
|
2334
2486
|
)
|
|
2335
2487
|
}
|
|
2336
2488
|
|
|
2337
|
-
override fun
|
|
2489
|
+
override fun handleServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
2490
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2491
|
+
return
|
|
2492
|
+
}
|
|
2338
2493
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2339
2494
|
Bridge.log("LIVE: GATT services discovered")
|
|
2340
2495
|
|
|
@@ -2397,7 +2552,17 @@ class MentraLive : SGCManager() {
|
|
|
2397
2552
|
// This ensures no concurrent GATT operations on older Android BLE
|
|
2398
2553
|
// stacks.
|
|
2399
2554
|
if (checkPermission()) {
|
|
2400
|
-
val
|
|
2555
|
+
val mtuToken = mtuSetupGate.begin()
|
|
2556
|
+
mtuSetupToken = mtuToken
|
|
2557
|
+
val mtuRequested =
|
|
2558
|
+
try {
|
|
2559
|
+
gatt.requestMtu(512)
|
|
2560
|
+
} catch (e: SecurityException) {
|
|
2561
|
+
Bridge.log(
|
|
2562
|
+
"LIVE: ⚠️ MTU request denied; continuing with notification setup"
|
|
2563
|
+
)
|
|
2564
|
+
false
|
|
2565
|
+
}
|
|
2401
2566
|
Bridge.log(
|
|
2402
2567
|
"LIVE: 🔄 Requested MTU size 512, success: " +
|
|
2403
2568
|
mtuRequested
|
|
@@ -2405,12 +2570,18 @@ class MentraLive : SGCManager() {
|
|
|
2405
2570
|
if (!mtuRequested) {
|
|
2406
2571
|
// MTU request failed to even start, enable notifications
|
|
2407
2572
|
// directly
|
|
2408
|
-
|
|
2573
|
+
completeMtuSetup(
|
|
2574
|
+
gatt,
|
|
2575
|
+
mtuToken,
|
|
2576
|
+
"request_not_started"
|
|
2577
|
+
)
|
|
2578
|
+
} else {
|
|
2579
|
+
startMtuWatchdog(gatt, mtuToken)
|
|
2409
2580
|
}
|
|
2410
2581
|
// Otherwise, enableNotifications() will be called from
|
|
2411
2582
|
// onMtuChanged
|
|
2412
2583
|
} else {
|
|
2413
|
-
|
|
2584
|
+
enableNotificationsAfterMtu(gatt, "permission_fallback")
|
|
2414
2585
|
}
|
|
2415
2586
|
|
|
2416
2587
|
// NOTE: Send queue and readiness loop are started AFTER descriptor
|
|
@@ -2437,11 +2608,14 @@ class MentraLive : SGCManager() {
|
|
|
2437
2608
|
}
|
|
2438
2609
|
}
|
|
2439
2610
|
|
|
2440
|
-
override fun
|
|
2611
|
+
override fun handleCharacteristicRead(
|
|
2441
2612
|
gatt: BluetoothGatt,
|
|
2442
2613
|
characteristic: BluetoothGattCharacteristic,
|
|
2443
2614
|
status: Int
|
|
2444
2615
|
) {
|
|
2616
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2617
|
+
return
|
|
2618
|
+
}
|
|
2445
2619
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2446
2620
|
Bridge.log("LIVE: Characteristic read successful")
|
|
2447
2621
|
// Process the read data if needed
|
|
@@ -2450,7 +2624,10 @@ class MentraLive : SGCManager() {
|
|
|
2450
2624
|
}
|
|
2451
2625
|
}
|
|
2452
2626
|
|
|
2453
|
-
override fun
|
|
2627
|
+
override fun handleReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
|
2628
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2629
|
+
return
|
|
2630
|
+
}
|
|
2454
2631
|
rssiReadInProgress = false
|
|
2455
2632
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2456
2633
|
if (isConnected && bluetoothGatt != null && gatt === bluetoothGatt) {
|
|
@@ -2461,11 +2638,14 @@ class MentraLive : SGCManager() {
|
|
|
2461
2638
|
}
|
|
2462
2639
|
}
|
|
2463
2640
|
|
|
2464
|
-
override fun
|
|
2641
|
+
override fun handleCharacteristicWrite(
|
|
2465
2642
|
gatt: BluetoothGatt,
|
|
2466
2643
|
characteristic: BluetoothGattCharacteristic,
|
|
2467
2644
|
status: Int
|
|
2468
2645
|
) {
|
|
2646
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2647
|
+
return
|
|
2648
|
+
}
|
|
2469
2649
|
val trace = inFlightBleWriteTrace
|
|
2470
2650
|
val callbackAtMs = System.currentTimeMillis()
|
|
2471
2651
|
val callbackDelayMs =
|
|
@@ -2527,11 +2707,12 @@ class MentraLive : SGCManager() {
|
|
|
2527
2707
|
}
|
|
2528
2708
|
}
|
|
2529
2709
|
|
|
2530
|
-
override fun
|
|
2710
|
+
override fun handleCharacteristicChanged(
|
|
2531
2711
|
gatt: BluetoothGatt,
|
|
2532
|
-
characteristic: BluetoothGattCharacteristic
|
|
2712
|
+
characteristic: BluetoothGattCharacteristic,
|
|
2713
|
+
data: ByteArray
|
|
2533
2714
|
) {
|
|
2534
|
-
if (!
|
|
2715
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2535
2716
|
return
|
|
2536
2717
|
}
|
|
2537
2718
|
|
|
@@ -2539,8 +2720,7 @@ class MentraLive : SGCManager() {
|
|
|
2539
2720
|
val threadId = Thread.currentThread().id
|
|
2540
2721
|
val uuid = characteristic.uuid
|
|
2541
2722
|
|
|
2542
|
-
|
|
2543
|
-
if (data == null || data.isEmpty()) {
|
|
2723
|
+
if (data.isEmpty()) {
|
|
2544
2724
|
return
|
|
2545
2725
|
}
|
|
2546
2726
|
|
|
@@ -2584,11 +2764,14 @@ class MentraLive : SGCManager() {
|
|
|
2584
2764
|
processReceivedData(data, data.size)
|
|
2585
2765
|
}
|
|
2586
2766
|
|
|
2587
|
-
override fun
|
|
2767
|
+
override fun handleDescriptorWrite(
|
|
2588
2768
|
gatt: BluetoothGatt,
|
|
2589
2769
|
descriptor: BluetoothGattDescriptor,
|
|
2590
2770
|
status: Int
|
|
2591
2771
|
) {
|
|
2772
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2773
|
+
return
|
|
2774
|
+
}
|
|
2592
2775
|
val threadId = Thread.currentThread().id
|
|
2593
2776
|
|
|
2594
2777
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
@@ -2616,43 +2799,91 @@ class MentraLive : SGCManager() {
|
|
|
2616
2799
|
writeNextDescriptor()
|
|
2617
2800
|
}
|
|
2618
2801
|
|
|
2619
|
-
override fun
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2802
|
+
override fun handleMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
|
2803
|
+
handler.post {
|
|
2804
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2805
|
+
return@post
|
|
2806
|
+
}
|
|
2807
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2808
|
+
Bridge.log(
|
|
2809
|
+
"LIVE: 🔵 MTU negotiation successful - changed to " +
|
|
2810
|
+
mtu +
|
|
2811
|
+
" bytes"
|
|
2812
|
+
)
|
|
2813
|
+
val effectivePayload = mtu - 3
|
|
2814
|
+
Bridge.log(
|
|
2815
|
+
"LIVE: Effective payload size: " +
|
|
2816
|
+
effectivePayload +
|
|
2817
|
+
" bytes"
|
|
2818
|
+
)
|
|
2628
2819
|
|
|
2629
|
-
|
|
2630
|
-
currentMtu = mtu
|
|
2820
|
+
currentMtu = mtu
|
|
2631
2821
|
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2822
|
+
if (mtu >= 64) {
|
|
2823
|
+
Bridge.log(
|
|
2824
|
+
"LIVE: ✅ MTU size is sufficient for LC3 audio data packets"
|
|
2825
|
+
)
|
|
2826
|
+
} else {
|
|
2827
|
+
Log.w(
|
|
2828
|
+
TAG,
|
|
2829
|
+
"⚠️ MTU size may be too small for LC3 audio data packets"
|
|
2830
|
+
)
|
|
2831
|
+
Bridge.log(
|
|
2832
|
+
"LIVE: 📊 Effective MTU payload: " +
|
|
2833
|
+
effectivePayload +
|
|
2834
|
+
" bytes"
|
|
2835
|
+
)
|
|
2836
|
+
}
|
|
2636
2837
|
} else {
|
|
2637
|
-
Log.
|
|
2638
|
-
|
|
2639
|
-
|
|
2838
|
+
Log.e(TAG, "❌ MTU change failed with status: " + status)
|
|
2839
|
+
Log.w(
|
|
2840
|
+
TAG,
|
|
2841
|
+
" Will continue with default MTU (23 bytes, 20 byte payload)"
|
|
2640
2842
|
)
|
|
2641
2843
|
}
|
|
2642
|
-
} else {
|
|
2643
|
-
Log.e(TAG, "❌ MTU change failed with status: " + status)
|
|
2644
|
-
Log.w(TAG, " Will continue with default MTU (23 bytes, 20 byte payload)")
|
|
2645
|
-
}
|
|
2646
2844
|
|
|
2647
|
-
|
|
2648
|
-
// (descriptor writes are GATT operations and can't overlap with MTU request)
|
|
2649
|
-
if (!notificationsEnabled) {
|
|
2650
|
-
notificationsEnabled = true
|
|
2651
|
-
enableNotifications()
|
|
2845
|
+
mtuSetupToken?.let { completeMtuSetup(gatt, it, "callback") }
|
|
2652
2846
|
}
|
|
2653
2847
|
}
|
|
2654
2848
|
}
|
|
2655
2849
|
|
|
2850
|
+
private fun startMtuWatchdog(gatt: BluetoothGatt, token: Long) {
|
|
2851
|
+
mtuWatchdogRunnable?.let { gattLifecycleHandler.removeCallbacks(it) }
|
|
2852
|
+
val watchdog =
|
|
2853
|
+
Runnable {
|
|
2854
|
+
handler.post {
|
|
2855
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2856
|
+
return@post
|
|
2857
|
+
}
|
|
2858
|
+
Bridge.log(
|
|
2859
|
+
"LIVE: ⚠️ MTU callback timed out after ${MTU_CALLBACK_TIMEOUT_MS}ms; continuing with notification setup"
|
|
2860
|
+
)
|
|
2861
|
+
completeMtuSetup(gatt, token, "watchdog")
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
mtuWatchdogRunnable = watchdog
|
|
2865
|
+
gattLifecycleHandler.postDelayed(watchdog, MTU_CALLBACK_TIMEOUT_MS)
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
private fun completeMtuSetup(gatt: BluetoothGatt, token: Long, reason: String) {
|
|
2869
|
+
if (!mtuSetupGate.complete(token)) {
|
|
2870
|
+
return
|
|
2871
|
+
}
|
|
2872
|
+
mtuWatchdogRunnable?.let { gattLifecycleHandler.removeCallbacks(it) }
|
|
2873
|
+
mtuWatchdogRunnable = null
|
|
2874
|
+
mtuSetupToken = null
|
|
2875
|
+
enableNotificationsAfterMtu(gatt, reason)
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
private fun enableNotificationsAfterMtu(gatt: BluetoothGatt, reason: String) {
|
|
2879
|
+
if (!isActiveGattCallback(gatt) || notificationsEnabled) {
|
|
2880
|
+
return
|
|
2881
|
+
}
|
|
2882
|
+
notificationsEnabled = true
|
|
2883
|
+
Bridge.log("LIVE: 🔔 Continuing GATT setup after MTU ($reason)")
|
|
2884
|
+
enableNotifications()
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2656
2887
|
/**
|
|
2657
2888
|
* Write the next queued descriptor, or mark the queue as idle. Must be called after each
|
|
2658
2889
|
* onDescriptorWrite callback to serialize BLE GATT operations. On older Android devices,
|
|
@@ -2660,6 +2891,12 @@ class MentraLive : SGCManager() {
|
|
|
2660
2891
|
* subsequent writes to silently fail.
|
|
2661
2892
|
*/
|
|
2662
2893
|
private fun writeNextDescriptor() {
|
|
2894
|
+
val gatt = bluetoothGatt ?: return
|
|
2895
|
+
if (!isActiveGattCallback(gatt)) return
|
|
2896
|
+
val epoch = gattEpoch
|
|
2897
|
+
val continueSetup = Runnable {
|
|
2898
|
+
if (epoch == gattEpoch && isActiveGattCallback(gatt)) writeNextDescriptor()
|
|
2899
|
+
}
|
|
2663
2900
|
val next = pendingDescriptorWrites.poll()
|
|
2664
2901
|
if (next == null) {
|
|
2665
2902
|
isDescriptorWriteInProgress = false
|
|
@@ -2703,12 +2940,12 @@ class MentraLive : SGCManager() {
|
|
|
2703
2940
|
uuid +
|
|
2704
2941
|
", continuing queue"
|
|
2705
2942
|
)
|
|
2706
|
-
handler.postDelayed(
|
|
2943
|
+
handler.postDelayed(continueSetup, 50L)
|
|
2707
2944
|
}
|
|
2708
2945
|
} catch (e: Exception) {
|
|
2709
2946
|
val threadId = Thread.currentThread().id
|
|
2710
2947
|
Log.e(TAG, "Thread-" + threadId + ": ⚠️ Error writing descriptor: " + e.message)
|
|
2711
|
-
handler.postDelayed(
|
|
2948
|
+
handler.postDelayed(continueSetup, 50L)
|
|
2712
2949
|
}
|
|
2713
2950
|
}
|
|
2714
2951
|
|
|
@@ -3079,8 +3316,12 @@ class MentraLive : SGCManager() {
|
|
|
3079
3316
|
return messageId
|
|
3080
3317
|
}
|
|
3081
3318
|
|
|
3082
|
-
/** Send a JSON object to the glasses with message ID and ACK tracking */
|
|
3319
|
+
/** Send a JSON object to the glasses with message ID and ACK tracking. */
|
|
3083
3320
|
private fun sendJson(json: JSONObject?, wakeup: Boolean) {
|
|
3321
|
+
sendJson(json, wakeup, true)
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
private fun sendJson(json: JSONObject?, wakeup: Boolean, bridgeLogging: Boolean): Boolean {
|
|
3084
3325
|
if (json != null) {
|
|
3085
3326
|
try {
|
|
3086
3327
|
val sessionGeneration = bleSessionGeneration.get()
|
|
@@ -3099,7 +3340,7 @@ class MentraLive : SGCManager() {
|
|
|
3099
3340
|
json,
|
|
3100
3341
|
jsonStr.length
|
|
3101
3342
|
)
|
|
3102
|
-
sendDataToGlasses(jsonStr, wakeup, sessionGeneration)
|
|
3343
|
+
return sendDataToGlasses(jsonStr, wakeup, sessionGeneration, bridgeLogging)
|
|
3103
3344
|
} else {
|
|
3104
3345
|
// Add esoteric message ID to the JSON
|
|
3105
3346
|
val messageId = generateEsotericMessageId()
|
|
@@ -3124,12 +3365,13 @@ class MentraLive : SGCManager() {
|
|
|
3124
3365
|
// Calculate dynamic timeout for chunked message
|
|
3125
3366
|
val estimatedChunks = Math.ceil(jsonStr.length / 300.0).toInt()
|
|
3126
3367
|
ackTimeout = ACK_TIMEOUT_MS + (estimatedChunks * 50L) + 2000L
|
|
3127
|
-
|
|
3368
|
+
transportLog(
|
|
3128
3369
|
"LIVE: Message will be chunked into ~" +
|
|
3129
3370
|
estimatedChunks +
|
|
3130
3371
|
" chunks, using dynamic timeout: " +
|
|
3131
3372
|
ackTimeout +
|
|
3132
|
-
"ms"
|
|
3373
|
+
"ms",
|
|
3374
|
+
bridgeLogging,
|
|
3133
3375
|
)
|
|
3134
3376
|
}
|
|
3135
3377
|
} catch (e: JSONException) {
|
|
@@ -3141,7 +3383,13 @@ class MentraLive : SGCManager() {
|
|
|
3141
3383
|
}
|
|
3142
3384
|
|
|
3143
3385
|
// Track the message for ACK with appropriate timeout
|
|
3144
|
-
trackMessageForAck(
|
|
3386
|
+
trackMessageForAck(
|
|
3387
|
+
messageId,
|
|
3388
|
+
jsonStr,
|
|
3389
|
+
ackTimeout,
|
|
3390
|
+
sessionGeneration,
|
|
3391
|
+
bridgeLogging,
|
|
3392
|
+
)
|
|
3145
3393
|
|
|
3146
3394
|
// Send the data
|
|
3147
3395
|
if ("take_photo" == json.optString("type", "")) {
|
|
@@ -3160,7 +3408,9 @@ class MentraLive : SGCManager() {
|
|
|
3160
3408
|
json,
|
|
3161
3409
|
jsonStr.length
|
|
3162
3410
|
)
|
|
3163
|
-
sendDataToGlasses(jsonStr, wakeup, sessionGeneration)
|
|
3411
|
+
val accepted = sendDataToGlasses(jsonStr, wakeup, sessionGeneration, bridgeLogging)
|
|
3412
|
+
if (!accepted) pendingMessages.remove(messageId)
|
|
3413
|
+
return accepted
|
|
3164
3414
|
}
|
|
3165
3415
|
} catch (e: JSONException) {
|
|
3166
3416
|
Log.e(TAG, "Error adding message ID to JSON", e)
|
|
@@ -3168,6 +3418,7 @@ class MentraLive : SGCManager() {
|
|
|
3168
3418
|
} else {
|
|
3169
3419
|
Bridge.log("LIVE: Cannot send JSON to ASG, JSON is null")
|
|
3170
3420
|
}
|
|
3421
|
+
return false
|
|
3171
3422
|
}
|
|
3172
3423
|
|
|
3173
3424
|
private fun sendJson(json: JSONObject?) {
|
|
@@ -3185,20 +3436,25 @@ class MentraLive : SGCManager() {
|
|
|
3185
3436
|
messageId: Long,
|
|
3186
3437
|
messageData: String,
|
|
3187
3438
|
timeoutMs: Long,
|
|
3188
|
-
sessionGeneration: Long
|
|
3439
|
+
sessionGeneration: Long,
|
|
3440
|
+
bridgeLogging: Boolean = true,
|
|
3189
3441
|
) {
|
|
3190
3442
|
if (!isConnected || sessionGeneration != bleSessionGeneration.get()) {
|
|
3191
|
-
|
|
3443
|
+
transportLog(
|
|
3444
|
+
"LIVE: Not connected, skipping ACK tracking for message " + messageId,
|
|
3445
|
+
bridgeLogging,
|
|
3446
|
+
)
|
|
3192
3447
|
return
|
|
3193
3448
|
}
|
|
3194
3449
|
|
|
3195
3450
|
// Skip ACK tracking for glasses with build number < 5 (older firmware)
|
|
3196
3451
|
if (buildNumberInt < 5) {
|
|
3197
|
-
|
|
3452
|
+
transportLog(
|
|
3198
3453
|
"LIVE: Glasses build number (" +
|
|
3199
3454
|
buildNumberInt +
|
|
3200
3455
|
") < 5, skipping ACK tracking for message " +
|
|
3201
|
-
messageId
|
|
3456
|
+
messageId,
|
|
3457
|
+
bridgeLogging,
|
|
3202
3458
|
)
|
|
3203
3459
|
return
|
|
3204
3460
|
}
|
|
@@ -3220,11 +3476,20 @@ class MentraLive : SGCManager() {
|
|
|
3220
3476
|
// Schedule ACK timeout with custom timeout
|
|
3221
3477
|
handler.postDelayed({ checkMessageAck(messageId, sessionGeneration) }, timeoutMs)
|
|
3222
3478
|
|
|
3223
|
-
|
|
3224
|
-
"LIVE: 📋 Tracking message " + messageId + " for ACK (timeout: " + timeoutMs + "ms)"
|
|
3479
|
+
transportLog(
|
|
3480
|
+
"LIVE: 📋 Tracking message " + messageId + " for ACK (timeout: " + timeoutMs + "ms)",
|
|
3481
|
+
bridgeLogging,
|
|
3225
3482
|
)
|
|
3226
3483
|
}
|
|
3227
3484
|
|
|
3485
|
+
private fun transportLog(message: String, bridgeLogging: Boolean) {
|
|
3486
|
+
if (bridgeLogging) {
|
|
3487
|
+
Bridge.log(message)
|
|
3488
|
+
} else {
|
|
3489
|
+
Log.d(TAG, message)
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3228
3493
|
/**
|
|
3229
3494
|
* Ends the current BLE send session before clearing its ACK state. Scheduled callbacks capture
|
|
3230
3495
|
* the old generation, and queued retries retain it, so a retry racing this clear cannot become
|
|
@@ -3718,6 +3983,9 @@ class MentraLive : SGCManager() {
|
|
|
3718
3983
|
}
|
|
3719
3984
|
}
|
|
3720
3985
|
|
|
3986
|
+
val incomingMessageId = if (json.has("mId")) json.optLong("mId") else null
|
|
3987
|
+
incomingGlassesMessageAckId(type, incomingMessageId)?.let(::sendAckToGlasses)
|
|
3988
|
+
|
|
3721
3989
|
// Check if this is a K900 command format (has "C" field instead of "type")
|
|
3722
3990
|
if (json.has("C")) {
|
|
3723
3991
|
processK900JsonMessage(json)
|
|
@@ -3872,6 +4140,44 @@ class MentraLive : SGCManager() {
|
|
|
3872
4140
|
wifiError.takeIf { it.isNotEmpty() }
|
|
3873
4141
|
)
|
|
3874
4142
|
}
|
|
4143
|
+
"wifi_forget_result" -> {
|
|
4144
|
+
if (!wifiResponseEnvelopeIsValid(json.keys().asSequence().associateWith { json.get(it) }, allowLegacy = true)) return
|
|
4145
|
+
Bridge.sendWifiForgetResult(
|
|
4146
|
+
requestId = json.optString("requestId", ""),
|
|
4147
|
+
sid = json.optString("sid", ""),
|
|
4148
|
+
ssid = json.optString("ssid", ""),
|
|
4149
|
+
protocolVersion = json.optInt("protocol_version", 0),
|
|
4150
|
+
outcome = json.optString("outcome", ""),
|
|
4151
|
+
legacyDispatched =
|
|
4152
|
+
if (json.has("dispatched")) json.optBoolean("dispatched") else null,
|
|
4153
|
+
connected =
|
|
4154
|
+
if (json.has("connected")) json.optBoolean("connected") else null,
|
|
4155
|
+
currentSsid = json.optString("current_ssid", ""),
|
|
4156
|
+
localIp = json.optString("local_ip", ""),
|
|
4157
|
+
error = json.optString("error", "").ifEmpty { null },
|
|
4158
|
+
)
|
|
4159
|
+
}
|
|
4160
|
+
"saved_wifi_networks" -> {
|
|
4161
|
+
if (!wifiResponseEnvelopeIsValid(json.keys().asSequence().associateWith { json.get(it) }, allowLegacy = false)) return
|
|
4162
|
+
val networks = mutableListOf<String>()
|
|
4163
|
+
val networkArray = json.optJSONArray("networks") ?: return
|
|
4164
|
+
if (networkArray != null) {
|
|
4165
|
+
for (i in 0 until networkArray.length()) {
|
|
4166
|
+
val network = networkArray.opt(i) as? String ?: return
|
|
4167
|
+
network.takeIf { it.trim().isNotEmpty() }?.let {
|
|
4168
|
+
networks.add(it)
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
Bridge.sendSavedWifiNetworks(
|
|
4173
|
+
requestId = json.optString("requestId", ""),
|
|
4174
|
+
sid = json.optString("sid", ""),
|
|
4175
|
+
protocolVersion = json.optInt("protocol_version", 0),
|
|
4176
|
+
outcome = json.optString("outcome", ""),
|
|
4177
|
+
networks = networks,
|
|
4178
|
+
error = json.optString("error", "").ifEmpty { null },
|
|
4179
|
+
)
|
|
4180
|
+
}
|
|
3875
4181
|
"hotspot_status_update" -> {
|
|
3876
4182
|
// Process hotspot status information (same pattern as "wifi_status")
|
|
3877
4183
|
val hotspotEnabled = json.optBoolean("hotspot_enabled", false)
|
|
@@ -4253,7 +4559,11 @@ class MentraLive : SGCManager() {
|
|
|
4253
4559
|
// Record the session id BEFORE marking ready: an unsolicited glasses_ready
|
|
4254
4560
|
// already runs this full remote-reset flow, so recording (not re-triggering)
|
|
4255
4561
|
// is correct here; version_info detection covers the restart case.
|
|
4256
|
-
json.optString("sid", "").takeIf { it.isNotEmpty() }
|
|
4562
|
+
glassesSessionId = json.optString("sid", "").takeIf { it.isNotEmpty() }
|
|
4563
|
+
Bridge.sendTypedMessage(
|
|
4564
|
+
"wifi_protocol_session_ready",
|
|
4565
|
+
mapOf("sid" to (glassesSessionId ?: "")),
|
|
4566
|
+
)
|
|
4257
4567
|
readinessCompletedThisBleSession = true
|
|
4258
4568
|
|
|
4259
4569
|
// Set the ready flag to stop any future readiness checks
|
|
@@ -4447,6 +4757,7 @@ class MentraLive : SGCManager() {
|
|
|
4447
4757
|
}
|
|
4448
4758
|
|
|
4449
4759
|
val versionInfoLegacy = HashMap<String, Any>()
|
|
4760
|
+
versionInfoLegacy["version_info_type"] = "version_info"
|
|
4450
4761
|
versionInfoLegacy["appVersion"] = appVersionLegacy
|
|
4451
4762
|
versionInfoLegacy["buildNumber"] = buildNumberLegacy
|
|
4452
4763
|
versionInfoLegacy["deviceModel"] = deviceModelLegacy
|
|
@@ -4630,6 +4941,7 @@ class MentraLive : SGCManager() {
|
|
|
4630
4941
|
|
|
4631
4942
|
// Extract all fields from JSON (except "type")
|
|
4632
4943
|
val fields = HashMap<String, Any>()
|
|
4944
|
+
fields["version_info_type"] = type
|
|
4633
4945
|
val keys = json.keys()
|
|
4634
4946
|
while (keys.hasNext()) {
|
|
4635
4947
|
val key = keys.next()
|
|
@@ -4743,7 +5055,7 @@ class MentraLive : SGCManager() {
|
|
|
4743
5055
|
handleGlassesSessionId(json)
|
|
4744
5056
|
|
|
4745
5057
|
Bridge.log("LIVE: Processed version_info fields and sent to RN")
|
|
4746
|
-
Bridge.sendVersionInfo(fields)
|
|
5058
|
+
Bridge.sendVersionInfo(fields, type)
|
|
4747
5059
|
} else {
|
|
4748
5060
|
Log.d(TAG, "📦 Unknown message type: " + type)
|
|
4749
5061
|
}
|
|
@@ -5664,9 +5976,14 @@ class MentraLive : SGCManager() {
|
|
|
5664
5976
|
* build number, firmware version, etc.
|
|
5665
5977
|
*/
|
|
5666
5978
|
override fun requestVersionInfo() {
|
|
5979
|
+
requestVersionInfo(null)
|
|
5980
|
+
}
|
|
5981
|
+
|
|
5982
|
+
fun requestVersionInfo(requestId: String?) {
|
|
5667
5983
|
try {
|
|
5668
5984
|
val json = JSONObject()
|
|
5669
5985
|
json.put("type", "request_version")
|
|
5986
|
+
requestId?.let { json.put("request_id", it) }
|
|
5670
5987
|
sendJson(json, false)
|
|
5671
5988
|
Bridge.log("LIVE: 📱 Requesting version info from glasses")
|
|
5672
5989
|
} catch (e: JSONException) {
|
|
@@ -5938,6 +6255,8 @@ class MentraLive : SGCManager() {
|
|
|
5938
6255
|
// SmartGlassesCommunicator interface implementation
|
|
5939
6256
|
|
|
5940
6257
|
override fun findCompatibleDevices() {
|
|
6258
|
+
if (postGattLifecycle { findCompatibleDevices() }) return
|
|
6259
|
+
connectionRequestEpoch++
|
|
5941
6260
|
Bridge.log("LIVE: Finding compatible Mentra Live glasses")
|
|
5942
6261
|
|
|
5943
6262
|
// A fresh user scan owns the radio. Invalidate delayed reconnect callbacks
|
|
@@ -6002,6 +6321,7 @@ class MentraLive : SGCManager() {
|
|
|
6002
6321
|
}
|
|
6003
6322
|
|
|
6004
6323
|
override fun connectById(id: String) {
|
|
6324
|
+
if (postGattLifecycle { connectById(id) }) return
|
|
6005
6325
|
if (pairingYieldActive) {
|
|
6006
6326
|
Bridge.log("LIVE: connectById blocked — pairing yield active")
|
|
6007
6327
|
return
|
|
@@ -6031,6 +6351,7 @@ class MentraLive : SGCManager() {
|
|
|
6031
6351
|
}
|
|
6032
6352
|
|
|
6033
6353
|
override fun forget() {
|
|
6354
|
+
if (postGattLifecycle { forget() }) return
|
|
6034
6355
|
Bridge.log("LIVE: Forgetting Mentra Live glasses")
|
|
6035
6356
|
|
|
6036
6357
|
// Clear saved device name so a leftover name can't bypass the pairing-mode
|
|
@@ -6075,6 +6396,7 @@ class MentraLive : SGCManager() {
|
|
|
6075
6396
|
}
|
|
6076
6397
|
|
|
6077
6398
|
override fun disconnect() {
|
|
6399
|
+
if (postGattLifecycle { disconnect() }) return
|
|
6078
6400
|
if (unpairFlushPending) {
|
|
6079
6401
|
Bridge.log("LIVE: disconnect deferred — waiting for unpair write")
|
|
6080
6402
|
return
|
|
@@ -6116,6 +6438,7 @@ class MentraLive : SGCManager() {
|
|
|
6116
6438
|
}
|
|
6117
6439
|
|
|
6118
6440
|
fun connectToSmartGlasses() {
|
|
6441
|
+
if (postGattLifecycle { connectToSmartGlasses() }) return
|
|
6119
6442
|
if (pairingYieldActive) {
|
|
6120
6443
|
Bridge.log("LIVE: connectToSmartGlasses blocked — pairing yield active")
|
|
6121
6444
|
return
|
|
@@ -6741,7 +7064,16 @@ class MentraLive : SGCManager() {
|
|
|
6741
7064
|
"ctkd_bond_none",
|
|
6742
7065
|
"prev=$previousBondState queueSize=${sendQueue.size}"
|
|
6743
7066
|
)
|
|
6744
|
-
|
|
7067
|
+
if (previousBondState == BluetoothDevice.BOND_BONDED) {
|
|
7068
|
+
// A real unpair ends this Classic session. Invalidate
|
|
7069
|
+
// the target so delayed profile broadcasts cannot
|
|
7070
|
+
// make pairing look connected again.
|
|
7071
|
+
classicAudioConnectionTracker.invalidate(device.address)
|
|
7072
|
+
} else {
|
|
7073
|
+
// A failed/cancelled bond may be retried below, so keep
|
|
7074
|
+
// the target while clearing any partial profile state.
|
|
7075
|
+
classicAudioConnectionTracker.clear(device.address)
|
|
7076
|
+
}
|
|
6745
7077
|
audioConnected = false
|
|
6746
7078
|
if (previousBondState == BluetoothDevice.BOND_BONDING) {
|
|
6747
7079
|
// User cancelled or bonding failed - retry up to
|
|
@@ -6841,6 +7173,124 @@ class MentraLive : SGCManager() {
|
|
|
6841
7173
|
}
|
|
6842
7174
|
}
|
|
6843
7175
|
|
|
7176
|
+
/** Observe the two Android Classic audio profiles that Mentra Live exposes. */
|
|
7177
|
+
private fun initializeClassicAudioReceiver() {
|
|
7178
|
+
classicAudioReceiver =
|
|
7179
|
+
object : BroadcastReceiver() {
|
|
7180
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
7181
|
+
if (intent?.action != BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED &&
|
|
7182
|
+
intent?.action != BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) return
|
|
7183
|
+
val device = intent.bluetoothDeviceExtra() ?: return
|
|
7184
|
+
refreshClassicAudioConnectionState(device)
|
|
7185
|
+
}
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
|
|
7189
|
+
private fun registerClassicAudioReceiver() {
|
|
7190
|
+
val ctx = context ?: return
|
|
7191
|
+
val receiver = classicAudioReceiver ?: return
|
|
7192
|
+
if (isClassicAudioReceiverRegistered) return
|
|
7193
|
+
|
|
7194
|
+
try {
|
|
7195
|
+
val filter =
|
|
7196
|
+
IntentFilter().apply {
|
|
7197
|
+
addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)
|
|
7198
|
+
addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
|
|
7199
|
+
}
|
|
7200
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
7201
|
+
ctx.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
|
|
7202
|
+
} else {
|
|
7203
|
+
ctx.registerReceiver(receiver, filter)
|
|
7204
|
+
}
|
|
7205
|
+
isClassicAudioReceiverRegistered = true
|
|
7206
|
+
Bridge.log("LIVE: Classic: A2DP/HFP status receiver registered")
|
|
7207
|
+
} catch (e: Exception) {
|
|
7208
|
+
Bridge.log("LIVE: Classic: failed to register status receiver: " + e.message)
|
|
7209
|
+
}
|
|
7210
|
+
}
|
|
7211
|
+
|
|
7212
|
+
private fun unregisterClassicAudioReceiver() {
|
|
7213
|
+
if (!isClassicAudioReceiverRegistered || classicAudioReceiver == null) return
|
|
7214
|
+
|
|
7215
|
+
try {
|
|
7216
|
+
context?.unregisterReceiver(classicAudioReceiver)
|
|
7217
|
+
} catch (e: Exception) {
|
|
7218
|
+
Bridge.log("LIVE: Classic: failed to unregister status receiver: " + e.message)
|
|
7219
|
+
} finally {
|
|
7220
|
+
isClassicAudioReceiverRegistered = false
|
|
7221
|
+
}
|
|
7222
|
+
}
|
|
7223
|
+
|
|
7224
|
+
/**
|
|
7225
|
+
* Broadcast payloads are hints, never current truth (even when the MAC matches). Query both
|
|
7226
|
+
* profiles and publish one combined snapshot. A newer refresh or teardown invalidates all
|
|
7227
|
+
* outstanding results, including a previous connection to the same physical glasses.
|
|
7228
|
+
*/
|
|
7229
|
+
private fun refreshClassicAudioConnectionState(device: BluetoothDevice) {
|
|
7230
|
+
if (Looper.myLooper() != handler.looper) {
|
|
7231
|
+
handler.post { refreshClassicAudioConnectionState(device) }
|
|
7232
|
+
return
|
|
7233
|
+
}
|
|
7234
|
+
if (isKilled) return
|
|
7235
|
+
val adapter = bluetoothAdapter ?: return
|
|
7236
|
+
val ctx = context ?: return
|
|
7237
|
+
val ticket = classicAudioConnectionTracker.beginSnapshot(device.address) ?: return
|
|
7238
|
+
val states = mutableMapOf<ClassicAudioProfile, Boolean>()
|
|
7239
|
+
for ((profileId, audioProfile) in listOf(
|
|
7240
|
+
BluetoothProfile.A2DP to ClassicAudioProfile.A2DP,
|
|
7241
|
+
BluetoothProfile.HEADSET to ClassicAudioProfile.HEADSET,
|
|
7242
|
+
)) {
|
|
7243
|
+
try {
|
|
7244
|
+
val requested = adapter.getProfileProxy(ctx, object : BluetoothProfile.ServiceListener {
|
|
7245
|
+
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
|
7246
|
+
// Close before dispatch: destroy() may discard queued handler work.
|
|
7247
|
+
val connectedState = try {
|
|
7248
|
+
if (profile != profileId) null
|
|
7249
|
+
else proxy.getConnectionState(device) == BluetoothProfile.STATE_CONNECTED
|
|
7250
|
+
} catch (e: Exception) {
|
|
7251
|
+
Bridge.log("LIVE: Classic: snapshot unavailable: " + e.message)
|
|
7252
|
+
null
|
|
7253
|
+
} finally {
|
|
7254
|
+
try { adapter.closeProfileProxy(profileId, proxy) }
|
|
7255
|
+
catch (_: Exception) {}
|
|
7256
|
+
}
|
|
7257
|
+
if (connectedState == null) return
|
|
7258
|
+
handler.post {
|
|
7259
|
+
if (isKilled || profile != profileId) return@post
|
|
7260
|
+
states[audioProfile] = connectedState
|
|
7261
|
+
val connected = readyClassicAudioSnapshot(states) ?: return@post
|
|
7262
|
+
classicAudioConnectionTracker.applySnapshot(
|
|
7263
|
+
ticket, device.address, connected) {
|
|
7264
|
+
if (ClassicAudioProfile.A2DP in connected) {
|
|
7265
|
+
markAudioConnected(device)
|
|
7266
|
+
} else if (states[ClassicAudioProfile.A2DP] == false && audioConnected) {
|
|
7267
|
+
audioConnected = false
|
|
7268
|
+
Bridge.sendAudioDisconnected()
|
|
7269
|
+
}
|
|
7270
|
+
}
|
|
7271
|
+
}
|
|
7272
|
+
}
|
|
7273
|
+
override fun onServiceDisconnected(profile: Int) {}
|
|
7274
|
+
}, profileId)
|
|
7275
|
+
if (!requested) {
|
|
7276
|
+
// Unknown is not disconnected. A successful other-profile read can still
|
|
7277
|
+
// publish connected without waiting for this unavailable service.
|
|
7278
|
+
Bridge.log("LIVE: Classic: profile service unavailable: $profileId")
|
|
7279
|
+
}
|
|
7280
|
+
} catch (e: Exception) {
|
|
7281
|
+
Bridge.log("LIVE: Classic: profile query unavailable: " + e.message)
|
|
7282
|
+
}
|
|
7283
|
+
}
|
|
7284
|
+
}
|
|
7285
|
+
|
|
7286
|
+
@Suppress("DEPRECATION")
|
|
7287
|
+
private fun Intent.bluetoothDeviceExtra(): BluetoothDevice? =
|
|
7288
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
7289
|
+
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
|
7290
|
+
} else {
|
|
7291
|
+
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
|
7292
|
+
}
|
|
7293
|
+
|
|
6844
7294
|
/**
|
|
6845
7295
|
* Start CTKD Classic bonding / A2DP only after BLE notifications are ready. Calling this from
|
|
6846
7296
|
* onConnectionStateChange(CONNECTED) races dual-mode stacks and frequently yields GATT 19
|
|
@@ -6917,7 +7367,7 @@ class MentraLive : SGCManager() {
|
|
|
6917
7367
|
val method = device.javaClass.getMethod("removeBond")
|
|
6918
7368
|
val result = method.invoke(device) as Boolean
|
|
6919
7369
|
Bridge.log("LIVE: CTKD: Bond removal initiated, result: " + result)
|
|
6920
|
-
|
|
7370
|
+
classicAudioConnectionTracker.invalidate(device.address)
|
|
6921
7371
|
return result
|
|
6922
7372
|
} catch (e: Exception) {
|
|
6923
7373
|
Bridge.log("LIVE: CTKD: Error removing bond: " + e.message)
|
|
@@ -6927,7 +7377,7 @@ class MentraLive : SGCManager() {
|
|
|
6927
7377
|
|
|
6928
7378
|
/** Check if BT Classic is connected via CTKD */
|
|
6929
7379
|
fun isBtClassicConnected(): Boolean {
|
|
6930
|
-
return
|
|
7380
|
+
return classicAudioConnectionTracker.connected
|
|
6931
7381
|
}
|
|
6932
7382
|
|
|
6933
7383
|
/** A2DP profile service listener for connecting to already-bonded devices */
|
|
@@ -6986,11 +7436,11 @@ class MentraLive : SGCManager() {
|
|
|
6986
7436
|
try {
|
|
6987
7437
|
val state = a2dpProfile!!.getConnectionState(device)
|
|
6988
7438
|
Bridge.log("LIVE: A2DP: Current connection state: " + state)
|
|
7439
|
+
refreshClassicAudioConnectionState(device)
|
|
6989
7440
|
|
|
6990
7441
|
if (state == BluetoothProfile.STATE_CONNECTED) {
|
|
6991
7442
|
Bridge.log("LIVE: A2DP: Already connected to " + device.name)
|
|
6992
7443
|
a2dpConnectAttempts = 0
|
|
6993
|
-
markAudioConnected(device.name)
|
|
6994
7444
|
} else if (state == BluetoothProfile.STATE_DISCONNECTED) {
|
|
6995
7445
|
Bridge.log("LIVE: A2DP: Connecting to " + device.name)
|
|
6996
7446
|
// Use reflection to call connect() as it's a hidden API
|
|
@@ -7043,16 +7493,18 @@ class MentraLive : SGCManager() {
|
|
|
7043
7493
|
}
|
|
7044
7494
|
|
|
7045
7495
|
/** Helper to mark audio as connected and notify */
|
|
7046
|
-
private fun markAudioConnected(
|
|
7496
|
+
private fun markAudioConnected(device: BluetoothDevice) {
|
|
7047
7497
|
if (isKilled) {
|
|
7048
7498
|
Bridge.log(
|
|
7049
7499
|
"LIVE: A2DP: Ignoring markAudioConnected — SGC destroyed (would confuse DeviceManager)"
|
|
7050
7500
|
)
|
|
7051
7501
|
return
|
|
7052
7502
|
}
|
|
7053
|
-
|
|
7503
|
+
val wasAudioConnected = audioConnected
|
|
7054
7504
|
audioConnected = true
|
|
7055
|
-
|
|
7505
|
+
if (!wasAudioConnected) {
|
|
7506
|
+
Bridge.sendAudioConnected(safeDeviceName(device))
|
|
7507
|
+
}
|
|
7056
7508
|
if (glassesReadyReceived) {
|
|
7057
7509
|
Bridge.log(
|
|
7058
7510
|
"LIVE: A2DP: Both audio and glasses_ready confirmed - marking as fully connected"
|
|
@@ -7135,10 +7587,10 @@ class MentraLive : SGCManager() {
|
|
|
7135
7587
|
device.address +
|
|
7136
7588
|
")"
|
|
7137
7589
|
)
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7590
|
+
// The BLE device reference may already be cleared after a GATT drop. The tracker owns the
|
|
7591
|
+
// authoritative target address, so let it decide whether this teardown belongs to the
|
|
7592
|
+
// active Classic session instead of consulting connectedDevice.
|
|
7593
|
+
if (classicAudioConnectionTracker.invalidate(device.address)) {
|
|
7142
7594
|
audioConnected = false
|
|
7143
7595
|
}
|
|
7144
7596
|
|
|
@@ -7247,6 +7699,8 @@ class MentraLive : SGCManager() {
|
|
|
7247
7699
|
}
|
|
7248
7700
|
|
|
7249
7701
|
fun destroy() {
|
|
7702
|
+
if (postGattLifecycle { destroy() }) return
|
|
7703
|
+
connectionRequestEpoch++
|
|
7250
7704
|
Bridge.log("LIVE: Destroying MentraLiveSGC")
|
|
7251
7705
|
|
|
7252
7706
|
// Mark as killed to prevent reconnection attempts
|
|
@@ -7268,12 +7722,15 @@ class MentraLive : SGCManager() {
|
|
|
7268
7722
|
|
|
7269
7723
|
// CTKD Implementation: Unregister bonding receiver
|
|
7270
7724
|
unregisterBondingReceiver()
|
|
7725
|
+
unregisterClassicAudioReceiver()
|
|
7271
7726
|
|
|
7272
7727
|
// Tear down Classic (A2DP/HFP) before GATT close. Closing only the proxy left the
|
|
7273
7728
|
// phone's system Bluetooth UI showing Mentra Live still connected.
|
|
7274
7729
|
val classicDevice = connectedDevice
|
|
7275
7730
|
disconnectClassicProfiles(classicDevice)
|
|
7276
7731
|
closeA2dpProxy()
|
|
7732
|
+
classicAudioConnectionTracker.reset()
|
|
7733
|
+
DeviceStore.apply("glasses", "bluetoothClassicConnected", false)
|
|
7277
7734
|
|
|
7278
7735
|
// Stop readiness check loop
|
|
7279
7736
|
stopReadinessCheckLoop()
|
|
@@ -8628,9 +9085,10 @@ class MentraLive : SGCManager() {
|
|
|
8628
9085
|
wireBytes: Int,
|
|
8629
9086
|
packetCount: Int,
|
|
8630
9087
|
protocolVersion: Int,
|
|
8631
|
-
direction: String
|
|
9088
|
+
direction: String,
|
|
9089
|
+
bridgeLogging: Boolean = true,
|
|
8632
9090
|
) {
|
|
8633
|
-
|
|
9091
|
+
transportLog(
|
|
8634
9092
|
"BLE_TRACE direction=" +
|
|
8635
9093
|
direction +
|
|
8636
9094
|
" proto=v" +
|
|
@@ -8640,7 +9098,8 @@ class MentraLive : SGCManager() {
|
|
|
8640
9098
|
" wire=" +
|
|
8641
9099
|
wireBytes +
|
|
8642
9100
|
" packets=" +
|
|
8643
|
-
packetCount
|
|
9101
|
+
packetCount,
|
|
9102
|
+
bridgeLogging,
|
|
8644
9103
|
)
|
|
8645
9104
|
}
|
|
8646
9105
|
|
|
@@ -8658,15 +9117,19 @@ class MentraLive : SGCManager() {
|
|
|
8658
9117
|
private fun sendDataToGlasses(
|
|
8659
9118
|
data: String?,
|
|
8660
9119
|
wakeup: Boolean,
|
|
8661
|
-
sessionGeneration: Long
|
|
8662
|
-
|
|
9120
|
+
sessionGeneration: Long,
|
|
9121
|
+
bridgeLogging: Boolean = true,
|
|
9122
|
+
): Boolean {
|
|
8663
9123
|
if (data == null || data.isEmpty()) {
|
|
8664
9124
|
Log.e(TAG, "Cannot send empty data to glasses")
|
|
8665
|
-
return
|
|
9125
|
+
return false
|
|
8666
9126
|
}
|
|
8667
9127
|
if (sessionGeneration != bleSessionGeneration.get()) {
|
|
8668
|
-
|
|
8669
|
-
|
|
9128
|
+
transportLog(
|
|
9129
|
+
"LIVE: Dropping send request from stale BLE session $sessionGeneration",
|
|
9130
|
+
bridgeLogging,
|
|
9131
|
+
)
|
|
9132
|
+
return false
|
|
8670
9133
|
}
|
|
8671
9134
|
|
|
8672
9135
|
val wireData =
|
|
@@ -8694,14 +9157,14 @@ class MentraLive : SGCManager() {
|
|
|
8694
9157
|
}
|
|
8695
9158
|
|
|
8696
9159
|
if (useBinaryWireProtocol && buildNumberInt >= 5) {
|
|
8697
|
-
sendDataToGlassesBinary(
|
|
9160
|
+
return sendDataToGlassesBinary(
|
|
8698
9161
|
wireData,
|
|
8699
9162
|
wakeup,
|
|
8700
9163
|
commandTraceInfo,
|
|
8701
9164
|
isPhotoRequest,
|
|
8702
|
-
sessionGeneration
|
|
9165
|
+
sessionGeneration,
|
|
9166
|
+
bridgeLogging,
|
|
8703
9167
|
)
|
|
8704
|
-
return
|
|
8705
9168
|
}
|
|
8706
9169
|
|
|
8707
9170
|
// First check if the message needs chunking
|
|
@@ -8715,7 +9178,7 @@ class MentraLive : SGCManager() {
|
|
|
8715
9178
|
|
|
8716
9179
|
// Check if chunking is needed
|
|
8717
9180
|
if (MessageChunker.needsChunking(testWrappedJson)) {
|
|
8718
|
-
|
|
9181
|
+
transportLog("LIVE: Message exceeds threshold, chunking required", bridgeLogging)
|
|
8719
9182
|
if (isPhotoRequest) {
|
|
8720
9183
|
Bridge.log(
|
|
8721
9184
|
"LIVE: PHOTO PIPELINE BLE handoff — chunking enabled for request payload"
|
|
@@ -8733,7 +9196,8 @@ class MentraLive : SGCManager() {
|
|
|
8733
9196
|
|
|
8734
9197
|
// Create chunks
|
|
8735
9198
|
val chunks = MessageChunker.createChunks(wireData, messageId, wakeup)
|
|
8736
|
-
|
|
9199
|
+
if (chunks.isEmpty()) return false
|
|
9200
|
+
transportLog("LIVE: Sending " + chunks.size + " chunks", bridgeLogging)
|
|
8737
9201
|
if (isPhotoRequest) {
|
|
8738
9202
|
Bridge.log(
|
|
8739
9203
|
"LIVE: PHOTO PIPELINE BLE handoff — created " +
|
|
@@ -8791,13 +9255,13 @@ class MentraLive : SGCManager() {
|
|
|
8791
9255
|
}
|
|
8792
9256
|
}
|
|
8793
9257
|
|
|
8794
|
-
|
|
9258
|
+
transportLog("LIVE: All chunks queued for transmission", bridgeLogging)
|
|
8795
9259
|
if (isPhotoRequest) {
|
|
8796
9260
|
Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — all photo chunks queued")
|
|
8797
9261
|
}
|
|
8798
9262
|
} else {
|
|
8799
9263
|
// Normal single message transmission
|
|
8800
|
-
|
|
9264
|
+
transportLog("LIVE: Sending data to glasses: " + wireData, bridgeLogging)
|
|
8801
9265
|
|
|
8802
9266
|
// Pack the data using the centralized utility with the negotiated endianness
|
|
8803
9267
|
val packedData =
|
|
@@ -8831,7 +9295,9 @@ class MentraLive : SGCManager() {
|
|
|
8831
9295
|
}
|
|
8832
9296
|
} catch (e: Exception) {
|
|
8833
9297
|
Log.e(TAG, "Error creating data JSON", e)
|
|
9298
|
+
return false
|
|
8834
9299
|
}
|
|
9300
|
+
return sessionGeneration == bleSessionGeneration.get()
|
|
8835
9301
|
}
|
|
8836
9302
|
|
|
8837
9303
|
private fun sendDataToGlassesBinary(
|
|
@@ -8839,8 +9305,9 @@ class MentraLive : SGCManager() {
|
|
|
8839
9305
|
wakeup: Boolean,
|
|
8840
9306
|
commandTraceInfo: OutgoingBleCommandTraceInfo,
|
|
8841
9307
|
isPhotoRequest: Boolean,
|
|
8842
|
-
sessionGeneration: Long
|
|
8843
|
-
|
|
9308
|
+
sessionGeneration: Long,
|
|
9309
|
+
bridgeLogging: Boolean,
|
|
9310
|
+
): Boolean {
|
|
8844
9311
|
val payloadBytes = data.toByteArray(StandardCharsets.UTF_8)
|
|
8845
9312
|
var messageId = 0
|
|
8846
9313
|
var ackRequested = false
|
|
@@ -8859,6 +9326,7 @@ class MentraLive : SGCManager() {
|
|
|
8859
9326
|
wakeup,
|
|
8860
9327
|
ackRequested
|
|
8861
9328
|
)
|
|
9329
|
+
if (fragments.isEmpty()) return false
|
|
8862
9330
|
var totalWireBytes = 0
|
|
8863
9331
|
for (i in fragments.indices) {
|
|
8864
9332
|
val fragment = fragments[i]
|
|
@@ -8907,7 +9375,8 @@ class MentraLive : SGCManager() {
|
|
|
8907
9375
|
totalWireBytes,
|
|
8908
9376
|
fragments.size,
|
|
8909
9377
|
BleWireProtocol.PROTOCOL_V2,
|
|
8910
|
-
"phone_to_glasses"
|
|
9378
|
+
"phone_to_glasses",
|
|
9379
|
+
bridgeLogging,
|
|
8911
9380
|
)
|
|
8912
9381
|
|
|
8913
9382
|
if (isPhotoRequest) {
|
|
@@ -8918,6 +9387,7 @@ class MentraLive : SGCManager() {
|
|
|
8918
9387
|
totalWireBytes
|
|
8919
9388
|
)
|
|
8920
9389
|
}
|
|
9390
|
+
return sessionGeneration == bleSessionGeneration.get()
|
|
8921
9391
|
}
|
|
8922
9392
|
|
|
8923
9393
|
private fun parseOutgoingBleCommandTraceInfo(payload: String): OutgoingBleCommandTraceInfo {
|
|
@@ -9199,16 +9669,45 @@ class MentraLive : SGCManager() {
|
|
|
9199
9669
|
* K900 SystemUI can properly clear the cached credentials
|
|
9200
9670
|
*/
|
|
9201
9671
|
override fun forgetWifiNetwork(ssid: String) {
|
|
9202
|
-
|
|
9672
|
+
forgetWifiNetwork(ssid, null, null)
|
|
9673
|
+
}
|
|
9674
|
+
|
|
9675
|
+
override fun forgetWifiNetwork(ssid: String, requestId: String?, sid: String?): Boolean {
|
|
9676
|
+
if (!isConnected || bluetoothGatt == null || txCharacteristic == null || ssid.isBlank()) return false
|
|
9677
|
+
if ((requestId == null) != (sid == null)) return false
|
|
9678
|
+
if (requestId != null && (requestId.isBlank() || sid.isNullOrBlank())) return false
|
|
9679
|
+
Log.d(TAG, "LIVE: 📶 Sending WiFi forget command for SSID: " + ssid)
|
|
9203
9680
|
|
|
9204
9681
|
try {
|
|
9205
9682
|
val wifiCommand = JSONObject()
|
|
9206
9683
|
wifiCommand.put("type", "forget_wifi")
|
|
9207
9684
|
wifiCommand.put("ssid", ssid)
|
|
9208
|
-
|
|
9685
|
+
if (!requestId.isNullOrEmpty()) {
|
|
9686
|
+
wifiCommand.put("requestId", requestId)
|
|
9687
|
+
}
|
|
9688
|
+
if (!sid.isNullOrEmpty()) {
|
|
9689
|
+
wifiCommand.put("sid", sid)
|
|
9690
|
+
wifiCommand.put("protocolVersion", 1)
|
|
9691
|
+
}
|
|
9692
|
+
// This dispatch runs under the WiFi lifecycle lock. Keep its transport logs
|
|
9693
|
+
// native-only so a public log listener cannot re-enter reset before it is queued.
|
|
9694
|
+
return sendJson(wifiCommand, true, bridgeLogging = false)
|
|
9209
9695
|
} catch (e: JSONException) {
|
|
9210
9696
|
Log.e(TAG, "Error creating WiFi forget JSON", e)
|
|
9211
9697
|
}
|
|
9698
|
+
return false
|
|
9699
|
+
}
|
|
9700
|
+
|
|
9701
|
+
override fun requestSavedWifiNetworks(requestId: String, sid: String): Boolean {
|
|
9702
|
+
if (!isConnected || bluetoothGatt == null || txCharacteristic == null || requestId.isBlank() || sid.isBlank()) return false
|
|
9703
|
+
val command = JSONObject()
|
|
9704
|
+
command.put("type", "request_saved_wifi_networks")
|
|
9705
|
+
command.put("requestId", requestId)
|
|
9706
|
+
command.put("protocolVersion", 1)
|
|
9707
|
+
if (sid.isNotEmpty()) {
|
|
9708
|
+
command.put("sid", sid)
|
|
9709
|
+
}
|
|
9710
|
+
return sendJson(command, true, bridgeLogging = false)
|
|
9212
9711
|
}
|
|
9213
9712
|
|
|
9214
9713
|
override fun sendHotspotState(enabled: Boolean) {
|
|
@@ -9315,6 +9814,13 @@ class MentraLive : SGCManager() {
|
|
|
9315
9814
|
}
|
|
9316
9815
|
}
|
|
9317
9816
|
|
|
9817
|
+
private fun sendAckToGlasses(messageId: Long) {
|
|
9818
|
+
val ack = JSONObject()
|
|
9819
|
+
ack.put("type", "msg_ack")
|
|
9820
|
+
ack.put("mId", messageId)
|
|
9821
|
+
sendJsonWithoutAck(ack, false)
|
|
9822
|
+
}
|
|
9823
|
+
|
|
9318
9824
|
private fun sendJsonWithoutAck(json: JSONObject?) {
|
|
9319
9825
|
sendJsonWithoutAck(json, false)
|
|
9320
9826
|
}
|