@mentra/bluetooth-sdk 3.2.0-dev.195 → 3.2.0-dev.200
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 +19 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +14 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +7 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +33 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +88 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +50 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +314 -75
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattLifecycle.kt +64 -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 +2 -3
- 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/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/MentraLiveGattCallbackTest.kt +94 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattLifecycleTest.kt +112 -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/Bridge.swift +15 -1
- package/ios/Source/DeviceManager.swift +7 -3
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +32 -6
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +101 -0
- package/ios/Source/sgcs/MentraLive.swift +9 -2
- package/ios/Source/status/DeviceStatus.swift +2 -3
- package/ios/Tests/VersionInfoBridgeTests.swift +32 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +181 -0
- package/package.json +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -141,6 +141,9 @@ class MentraLive : SGCManager() {
|
|
|
141
141
|
private const val A2DP_CONNECT_MAX_ATTEMPTS = 5
|
|
142
142
|
private const val A2DP_CONNECT_RETRY_MS = 800L
|
|
143
143
|
private const val UNPAIR_FLUSH_MS = 400L
|
|
144
|
+
private const val GATT_DISCONNECT_TIMEOUT_MS = 2_000L
|
|
145
|
+
private const val MTU_CALLBACK_TIMEOUT_MS = 3_000L
|
|
146
|
+
private val gattTeardownBarrier = MentraLiveGattTeardownBarrier()
|
|
144
147
|
|
|
145
148
|
// L2CAP CoC fast path: new BES2700 firmware registers an LE L2CAP CoC server on this
|
|
146
149
|
// PSM. When the phone opens the channel, the glasses send file packets over it instead
|
|
@@ -564,6 +567,12 @@ class MentraLive : SGCManager() {
|
|
|
564
567
|
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
565
568
|
private var bluetoothScanner: BluetoothLeScanner? = null
|
|
566
569
|
@Volatile private var bluetoothGatt: BluetoothGatt? = null
|
|
570
|
+
private val gattLifecycleHandler = Handler(Looper.getMainLooper())
|
|
571
|
+
// Owned exclusively by the main looper, including callback validation and mutation.
|
|
572
|
+
private var gattEpoch = 0L
|
|
573
|
+
private var connectionRequestEpoch = 0L
|
|
574
|
+
@Volatile private var gattTeardownToken: Long? = null
|
|
575
|
+
@Volatile private var gattTeardownTimeoutRunnable: Runnable? = null
|
|
567
576
|
private var connectedDevice: BluetoothDevice? = null
|
|
568
577
|
private var txCharacteristic: BluetoothGattCharacteristic? = null
|
|
569
578
|
private var rxCharacteristic: BluetoothGattCharacteristic? = null
|
|
@@ -643,6 +652,9 @@ class MentraLive : SGCManager() {
|
|
|
643
652
|
private var isDescriptorWriteInProgress = false
|
|
644
653
|
private var notificationsEnabled =
|
|
645
654
|
false // Track if enableNotifications was already called this connection
|
|
655
|
+
private val mtuSetupGate = MentraLiveMtuSetupGate()
|
|
656
|
+
private var mtuSetupToken: Long? = null
|
|
657
|
+
private var mtuWatchdogRunnable: Runnable? = null
|
|
646
658
|
private var connectionTimeoutRunnable: Runnable? = null
|
|
647
659
|
private var connectionTimeoutHandler = Handler(Looper.getMainLooper())
|
|
648
660
|
private var processSendQueueRunnable: Runnable? = null
|
|
@@ -1233,6 +1245,7 @@ class MentraLive : SGCManager() {
|
|
|
1233
1245
|
|
|
1234
1246
|
/** Starts BLE scanning for Mentra Live glasses */
|
|
1235
1247
|
private fun startScan() {
|
|
1248
|
+
if (postGattLifecycle { startScan() }) return
|
|
1236
1249
|
if (pairingYieldActive) {
|
|
1237
1250
|
Bridge.log("LIVE: startScan blocked — pairing yield active")
|
|
1238
1251
|
return
|
|
@@ -1297,6 +1310,7 @@ class MentraLive : SGCManager() {
|
|
|
1297
1310
|
|
|
1298
1311
|
val generation = ++scanGeneration
|
|
1299
1312
|
isScanning = true
|
|
1313
|
+
scanCallback = createScanCallback(generation)
|
|
1300
1314
|
bluetoothScanner!!.startScan(filters, settings, scanCallback)
|
|
1301
1315
|
|
|
1302
1316
|
// Set a timeout to stop scanning
|
|
@@ -1349,6 +1363,7 @@ class MentraLive : SGCManager() {
|
|
|
1349
1363
|
|
|
1350
1364
|
/** Stops BLE scanning */
|
|
1351
1365
|
override fun stopScan() {
|
|
1366
|
+
if (postGattLifecycle { stopScan() }) return
|
|
1352
1367
|
manualDiscoveryActive = false
|
|
1353
1368
|
if (bluetoothAdapter == null || bluetoothScanner == null || !isScanning) {
|
|
1354
1369
|
return
|
|
@@ -1424,9 +1439,13 @@ class MentraLive : SGCManager() {
|
|
|
1424
1439
|
var seenDevices: MutableSet<String> = HashSet()
|
|
1425
1440
|
|
|
1426
1441
|
/** BLE Scan callback */
|
|
1427
|
-
private
|
|
1442
|
+
private var scanCallback: ScanCallback? = null
|
|
1443
|
+
|
|
1444
|
+
private fun createScanCallback(generation: Int): ScanCallback =
|
|
1428
1445
|
object : ScanCallback() {
|
|
1429
1446
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
1447
|
+
if (postGattLifecycle { onScanResult(callbackType, result) }) return
|
|
1448
|
+
if (!isScanning || generation != scanGeneration) return
|
|
1430
1449
|
// Check if the object has been destroyed to prevent NPE
|
|
1431
1450
|
if (context == null || isKilled) {
|
|
1432
1451
|
Bridge.log("LIVE: Ignoring scan result - object destroyed or killed")
|
|
@@ -1559,6 +1578,8 @@ class MentraLive : SGCManager() {
|
|
|
1559
1578
|
}
|
|
1560
1579
|
|
|
1561
1580
|
override fun onScanFailed(errorCode: Int) {
|
|
1581
|
+
if (postGattLifecycle { onScanFailed(errorCode) }) return
|
|
1582
|
+
if (generation != scanGeneration || !isScanning) return
|
|
1562
1583
|
Log.e(TAG, "BLE scan failed with error: " + errorCode)
|
|
1563
1584
|
isScanning = false
|
|
1564
1585
|
if (isReconnecting && !isKilled) {
|
|
@@ -1588,30 +1609,120 @@ class MentraLive : SGCManager() {
|
|
|
1588
1609
|
}
|
|
1589
1610
|
}
|
|
1590
1611
|
|
|
1591
|
-
/**
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1612
|
+
/** Return true when work was handed to the single process-wide lifecycle queue. */
|
|
1613
|
+
private fun postGattLifecycle(work: () -> Unit): Boolean {
|
|
1614
|
+
if (Looper.myLooper() == gattLifecycleHandler.looper) return false
|
|
1615
|
+
gattLifecycleHandler.post { work() }
|
|
1616
|
+
return true
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/** Safely close GATT, waiting for Android to finish a requested disconnect when possible. */
|
|
1597
1620
|
private fun closeGattQuietly(disconnect: Boolean) {
|
|
1621
|
+
check(Looper.myLooper() == gattLifecycleHandler.looper)
|
|
1598
1622
|
val gatt = bluetoothGatt
|
|
1599
|
-
bluetoothGatt = null
|
|
1600
1623
|
if (gatt == null) {
|
|
1601
1624
|
return
|
|
1602
1625
|
}
|
|
1626
|
+
cancelMtuWatchdog()
|
|
1627
|
+
if (disconnect) {
|
|
1628
|
+
beginGattTeardown(gatt)
|
|
1629
|
+
return
|
|
1630
|
+
}
|
|
1631
|
+
bluetoothGatt = null
|
|
1632
|
+
releaseGattSessionMedia()
|
|
1603
1633
|
try {
|
|
1604
|
-
|
|
1605
|
-
gatt.disconnect()
|
|
1606
|
-
}
|
|
1634
|
+
gatt.close()
|
|
1607
1635
|
} catch (e: Exception) {
|
|
1608
|
-
Log.w(TAG, "🔌 closeGattQuietly:
|
|
1636
|
+
Log.w(TAG, "🔌 closeGattQuietly: close threw " + e)
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/** Release side channels on every teardown, including missing disconnect callbacks. */
|
|
1641
|
+
private fun releaseGattSessionMedia() {
|
|
1642
|
+
closeL2capFileChannel()
|
|
1643
|
+
fileProcessingHandler.removeCallbacksAndMessages(null)
|
|
1644
|
+
clearFilePacketBuffer()
|
|
1645
|
+
closeLc3Logging()
|
|
1646
|
+
lc3AudioPlayer?.stopPlay()
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
private fun beginGattTeardown(gatt: BluetoothGatt) {
|
|
1650
|
+
check(Looper.myLooper() == gattLifecycleHandler.looper)
|
|
1651
|
+
if (gatt !== bluetoothGatt || gattTeardownToken != null) {
|
|
1652
|
+
return
|
|
1653
|
+
}
|
|
1654
|
+
val token = gattTeardownBarrier.beginTeardown()
|
|
1655
|
+
gattTeardownToken = token
|
|
1656
|
+
// Teardown owns session invalidation, including the timeout/replacement paths that do
|
|
1657
|
+
// not receive the normal remote-disconnect cleanup callback.
|
|
1658
|
+
cancelMtuWatchdog()
|
|
1659
|
+
isConnected = false
|
|
1660
|
+
isConnecting = false
|
|
1661
|
+
connectedDevice = null
|
|
1662
|
+
glassesReady = false
|
|
1663
|
+
glassesSessionId = null
|
|
1664
|
+
readinessCompletedThisBleSession = false
|
|
1665
|
+
glassesReadyReceived = false
|
|
1666
|
+
ctkdInitiatedThisGattSession = false
|
|
1667
|
+
audioConnected = false
|
|
1668
|
+
notificationsEnabled = false
|
|
1669
|
+
currentMtu = 23
|
|
1670
|
+
pendingDescriptorWrites.clear()
|
|
1671
|
+
isDescriptorWriteInProgress = false
|
|
1672
|
+
txCharacteristic = null
|
|
1673
|
+
rxCharacteristic = null
|
|
1674
|
+
lc3ReadCharacteristic = null
|
|
1675
|
+
lc3WriteCharacteristic = null
|
|
1676
|
+
handler.removeCallbacks(processSendQueueRunnable!!)
|
|
1677
|
+
stopReadinessCheckLoop()
|
|
1678
|
+
stopHeartbeat()
|
|
1679
|
+
stopSignalStrengthPolling()
|
|
1680
|
+
stopMicBeat()
|
|
1681
|
+
releaseGattSessionMedia()
|
|
1682
|
+
updateConnectionState(ConnTypes.DISCONNECTED)
|
|
1683
|
+
val timeout =
|
|
1684
|
+
Runnable {
|
|
1685
|
+
Bridge.log(
|
|
1686
|
+
"LIVE: 🔌 GATT disconnect callback timed out after ${GATT_DISCONNECT_TIMEOUT_MS}ms; closing transport"
|
|
1687
|
+
)
|
|
1688
|
+
completeGattTeardown(gatt, token, "timeout")
|
|
1689
|
+
}
|
|
1690
|
+
gattTeardownTimeoutRunnable = timeout
|
|
1691
|
+
gattLifecycleHandler.postDelayed(timeout, GATT_DISCONNECT_TIMEOUT_MS)
|
|
1692
|
+
try {
|
|
1693
|
+
Bridge.log("LIVE: 🔌 Waiting for GATT disconnected callback before close")
|
|
1694
|
+
gatt.disconnect()
|
|
1695
|
+
} catch (e: Exception) {
|
|
1696
|
+
Log.w(TAG, "🔌 GATT disconnect threw " + e)
|
|
1697
|
+
completeGattTeardown(gatt, token, "disconnect_exception")
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
private fun completeGattTeardown(gatt: BluetoothGatt, token: Long, reason: String) {
|
|
1702
|
+
check(Looper.myLooper() == gattLifecycleHandler.looper)
|
|
1703
|
+
if (gattTeardownToken != token) {
|
|
1704
|
+
return
|
|
1705
|
+
}
|
|
1706
|
+
gattTeardownTimeoutRunnable?.let { gattLifecycleHandler.removeCallbacks(it) }
|
|
1707
|
+
gattTeardownTimeoutRunnable = null
|
|
1708
|
+
gattTeardownToken = null
|
|
1709
|
+
if (gatt === bluetoothGatt) {
|
|
1710
|
+
bluetoothGatt = null
|
|
1609
1711
|
}
|
|
1610
1712
|
try {
|
|
1611
1713
|
gatt.close()
|
|
1612
1714
|
} catch (e: Exception) {
|
|
1613
|
-
Log.w(TAG, "🔌
|
|
1715
|
+
Log.w(TAG, "🔌 GATT close after $reason threw " + e)
|
|
1614
1716
|
}
|
|
1717
|
+
Bridge.log("LIVE: 🔌 GATT teardown complete ($reason)")
|
|
1718
|
+
gattTeardownBarrier.completeTeardown(token)
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
private fun cancelMtuWatchdog() {
|
|
1722
|
+
mtuWatchdogRunnable?.let { gattLifecycleHandler.removeCallbacks(it) }
|
|
1723
|
+
mtuWatchdogRunnable = null
|
|
1724
|
+
mtuSetupToken = null
|
|
1725
|
+
mtuSetupGate.cancel()
|
|
1615
1726
|
}
|
|
1616
1727
|
|
|
1617
1728
|
/**
|
|
@@ -1634,6 +1745,11 @@ class MentraLive : SGCManager() {
|
|
|
1634
1745
|
return false
|
|
1635
1746
|
}
|
|
1636
1747
|
|
|
1748
|
+
/** Ignore all non-disconnect work once this GATT is stale or tearing down. */
|
|
1749
|
+
private fun isActiveGattCallback(gatt: BluetoothGatt): Boolean {
|
|
1750
|
+
return isCurrentGattCallback(gatt) && gattTeardownToken == null
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1637
1753
|
private fun beginPairingTiming(reason: String) {
|
|
1638
1754
|
pairingTimingStartMs = SystemClock.elapsedRealtime()
|
|
1639
1755
|
pairingTimingLastMs = pairingTimingStartMs
|
|
@@ -1662,15 +1778,32 @@ class MentraLive : SGCManager() {
|
|
|
1662
1778
|
|
|
1663
1779
|
/** Connect to a specific BLE device */
|
|
1664
1780
|
private fun connectToDevice(device: BluetoothDevice?) {
|
|
1781
|
+
if (postGattLifecycle { connectToDevice(device) }) return
|
|
1665
1782
|
if (device == null) {
|
|
1666
1783
|
return
|
|
1667
1784
|
}
|
|
1668
|
-
if (pairingYieldActive) {
|
|
1785
|
+
if (isKilled || pairingYieldActive) {
|
|
1669
1786
|
Bridge.log("LIVE: connectToDevice blocked — pairing yield active")
|
|
1670
1787
|
isConnecting = false
|
|
1671
1788
|
isReconnecting = false
|
|
1672
1789
|
return
|
|
1673
1790
|
}
|
|
1791
|
+
val requestEpoch = ++connectionRequestEpoch
|
|
1792
|
+
// Never replace a still-open GATT. Its disconnect/close must finish first.
|
|
1793
|
+
bluetoothGatt?.let { beginGattTeardown(it) }
|
|
1794
|
+
gattTeardownBarrier.runWhenIdle {
|
|
1795
|
+
if (requestEpoch == connectionRequestEpoch && !isKilled && !pairingYieldActive) {
|
|
1796
|
+
connectToDeviceWhenIdle(device)
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
private fun connectToDeviceWhenIdle(device: BluetoothDevice) {
|
|
1802
|
+
check(Looper.myLooper() == gattLifecycleHandler.looper)
|
|
1803
|
+
check(bluetoothGatt == null)
|
|
1804
|
+
val epoch = ++gattEpoch
|
|
1805
|
+
currentMtu = 23
|
|
1806
|
+
val gattCallback = createGattCallback(epoch)
|
|
1674
1807
|
|
|
1675
1808
|
beginPairingTiming(
|
|
1676
1809
|
"connectToDevice addr=${device.address} name=${safeDeviceName(device)} attempt=$reconnectAttempts"
|
|
@@ -1683,7 +1816,7 @@ class MentraLive : SGCManager() {
|
|
|
1683
1816
|
|
|
1684
1817
|
// Set connection timeout
|
|
1685
1818
|
connectionTimeoutRunnable = Runnable {
|
|
1686
|
-
if (isConnecting && !isConnected) {
|
|
1819
|
+
if (epoch == gattEpoch && isConnecting && !isConnected) {
|
|
1687
1820
|
Log.w(
|
|
1688
1821
|
TAG,
|
|
1689
1822
|
"🔌 ⏰ CONNECTION TIMEOUT after " +
|
|
@@ -1775,6 +1908,8 @@ class MentraLive : SGCManager() {
|
|
|
1775
1908
|
// }
|
|
1776
1909
|
|
|
1777
1910
|
private fun enterPairingYield(windowMs: Long) {
|
|
1911
|
+
if (postGattLifecycle { enterPairingYield(windowMs) }) return
|
|
1912
|
+
connectionRequestEpoch++
|
|
1778
1913
|
pairingYieldActive = true
|
|
1779
1914
|
pairingYieldAwaitingReclaim = false
|
|
1780
1915
|
isReconnecting = false
|
|
@@ -1855,6 +1990,7 @@ class MentraLive : SGCManager() {
|
|
|
1855
1990
|
|
|
1856
1991
|
/** Handle reconnection with exponential backoff */
|
|
1857
1992
|
private fun handleReconnection() {
|
|
1993
|
+
if (postGattLifecycle { handleReconnection() }) return
|
|
1858
1994
|
// Don't attempt reconnection if we've been killed/forgotten
|
|
1859
1995
|
if (isKilled) {
|
|
1860
1996
|
Bridge.log("LIVE: 🔌 RECONNECT ABORTED - device has been killed/forgotten")
|
|
@@ -2093,9 +2229,13 @@ class MentraLive : SGCManager() {
|
|
|
2093
2229
|
}
|
|
2094
2230
|
|
|
2095
2231
|
/** GATT callback for BLE operations */
|
|
2096
|
-
private
|
|
2097
|
-
object :
|
|
2098
|
-
|
|
2232
|
+
private fun createGattCallback(epoch: Long): BluetoothGattCallback =
|
|
2233
|
+
object : SerializedGattCallback({ work ->
|
|
2234
|
+
// Always enqueue, even for an inline platform callback during connectGatt().
|
|
2235
|
+
// This lets connectGatt return and install the identity before validation.
|
|
2236
|
+
gattLifecycleHandler.post { work() }
|
|
2237
|
+
}, { gatt -> epoch == gattEpoch && isCurrentGattCallback(gatt) }) {
|
|
2238
|
+
override fun handleConnectionStateChange(
|
|
2099
2239
|
gatt: BluetoothGatt,
|
|
2100
2240
|
status: Int,
|
|
2101
2241
|
newState: Int
|
|
@@ -2103,6 +2243,19 @@ class MentraLive : SGCManager() {
|
|
|
2103
2243
|
if (!isCurrentGattCallback(gatt)) {
|
|
2104
2244
|
return
|
|
2105
2245
|
}
|
|
2246
|
+
val teardownToken = gattTeardownToken
|
|
2247
|
+
if (teardownToken != null) {
|
|
2248
|
+
if (
|
|
2249
|
+
newState == BluetoothProfile.STATE_DISCONNECTED ||
|
|
2250
|
+
status != BluetoothGatt.GATT_SUCCESS
|
|
2251
|
+
) {
|
|
2252
|
+
completeGattTeardown(gatt, teardownToken, "disconnected_callback")
|
|
2253
|
+
}
|
|
2254
|
+
// Once teardown starts, a queued successful CONNECTED callback belongs to
|
|
2255
|
+
// the closing transport. It must not resurrect application connection
|
|
2256
|
+
// state while the disconnect callback is still pending.
|
|
2257
|
+
return
|
|
2258
|
+
}
|
|
2106
2259
|
|
|
2107
2260
|
// Cancel the connection timeout
|
|
2108
2261
|
if (connectionTimeoutRunnable != null) {
|
|
@@ -2209,11 +2362,6 @@ class MentraLive : SGCManager() {
|
|
|
2209
2362
|
// Stop micbeat mechanism
|
|
2210
2363
|
stopMicBeat()
|
|
2211
2364
|
|
|
2212
|
-
// Close the L2CAP file channel (if the fast path was open)
|
|
2213
|
-
closeL2capFileChannel()
|
|
2214
|
-
fileProcessingHandler.removeCallbacksAndMessages(null)
|
|
2215
|
-
clearFilePacketBuffer()
|
|
2216
|
-
|
|
2217
2365
|
// Clean up GATT resources
|
|
2218
2366
|
closeGattQuietly(false)
|
|
2219
2367
|
|
|
@@ -2223,13 +2371,6 @@ class MentraLive : SGCManager() {
|
|
|
2223
2371
|
handleReconnection()
|
|
2224
2372
|
}
|
|
2225
2373
|
|
|
2226
|
-
// Close LC3 audio logging
|
|
2227
|
-
closeLc3Logging()
|
|
2228
|
-
|
|
2229
|
-
// stop LC3 player
|
|
2230
|
-
if (lc3AudioPlayer != null) {
|
|
2231
|
-
lc3AudioPlayer!!.stopPlay()
|
|
2232
|
-
}
|
|
2233
2374
|
}
|
|
2234
2375
|
} else {
|
|
2235
2376
|
// Connection error
|
|
@@ -2302,7 +2443,7 @@ class MentraLive : SGCManager() {
|
|
|
2302
2443
|
}
|
|
2303
2444
|
}
|
|
2304
2445
|
|
|
2305
|
-
override fun
|
|
2446
|
+
override fun handlePhyUpdate(
|
|
2306
2447
|
gatt: BluetoothGatt,
|
|
2307
2448
|
txPhy: Int,
|
|
2308
2449
|
rxPhy: Int,
|
|
@@ -2318,7 +2459,7 @@ class MentraLive : SGCManager() {
|
|
|
2318
2459
|
)
|
|
2319
2460
|
}
|
|
2320
2461
|
|
|
2321
|
-
override fun
|
|
2462
|
+
override fun handlePhyRead(
|
|
2322
2463
|
gatt: BluetoothGatt,
|
|
2323
2464
|
txPhy: Int,
|
|
2324
2465
|
rxPhy: Int,
|
|
@@ -2334,7 +2475,10 @@ class MentraLive : SGCManager() {
|
|
|
2334
2475
|
)
|
|
2335
2476
|
}
|
|
2336
2477
|
|
|
2337
|
-
override fun
|
|
2478
|
+
override fun handleServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
2479
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2480
|
+
return
|
|
2481
|
+
}
|
|
2338
2482
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2339
2483
|
Bridge.log("LIVE: GATT services discovered")
|
|
2340
2484
|
|
|
@@ -2397,7 +2541,17 @@ class MentraLive : SGCManager() {
|
|
|
2397
2541
|
// This ensures no concurrent GATT operations on older Android BLE
|
|
2398
2542
|
// stacks.
|
|
2399
2543
|
if (checkPermission()) {
|
|
2400
|
-
val
|
|
2544
|
+
val mtuToken = mtuSetupGate.begin()
|
|
2545
|
+
mtuSetupToken = mtuToken
|
|
2546
|
+
val mtuRequested =
|
|
2547
|
+
try {
|
|
2548
|
+
gatt.requestMtu(512)
|
|
2549
|
+
} catch (e: SecurityException) {
|
|
2550
|
+
Bridge.log(
|
|
2551
|
+
"LIVE: ⚠️ MTU request denied; continuing with notification setup"
|
|
2552
|
+
)
|
|
2553
|
+
false
|
|
2554
|
+
}
|
|
2401
2555
|
Bridge.log(
|
|
2402
2556
|
"LIVE: 🔄 Requested MTU size 512, success: " +
|
|
2403
2557
|
mtuRequested
|
|
@@ -2405,12 +2559,18 @@ class MentraLive : SGCManager() {
|
|
|
2405
2559
|
if (!mtuRequested) {
|
|
2406
2560
|
// MTU request failed to even start, enable notifications
|
|
2407
2561
|
// directly
|
|
2408
|
-
|
|
2562
|
+
completeMtuSetup(
|
|
2563
|
+
gatt,
|
|
2564
|
+
mtuToken,
|
|
2565
|
+
"request_not_started"
|
|
2566
|
+
)
|
|
2567
|
+
} else {
|
|
2568
|
+
startMtuWatchdog(gatt, mtuToken)
|
|
2409
2569
|
}
|
|
2410
2570
|
// Otherwise, enableNotifications() will be called from
|
|
2411
2571
|
// onMtuChanged
|
|
2412
2572
|
} else {
|
|
2413
|
-
|
|
2573
|
+
enableNotificationsAfterMtu(gatt, "permission_fallback")
|
|
2414
2574
|
}
|
|
2415
2575
|
|
|
2416
2576
|
// NOTE: Send queue and readiness loop are started AFTER descriptor
|
|
@@ -2437,11 +2597,14 @@ class MentraLive : SGCManager() {
|
|
|
2437
2597
|
}
|
|
2438
2598
|
}
|
|
2439
2599
|
|
|
2440
|
-
override fun
|
|
2600
|
+
override fun handleCharacteristicRead(
|
|
2441
2601
|
gatt: BluetoothGatt,
|
|
2442
2602
|
characteristic: BluetoothGattCharacteristic,
|
|
2443
2603
|
status: Int
|
|
2444
2604
|
) {
|
|
2605
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2606
|
+
return
|
|
2607
|
+
}
|
|
2445
2608
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2446
2609
|
Bridge.log("LIVE: Characteristic read successful")
|
|
2447
2610
|
// Process the read data if needed
|
|
@@ -2450,7 +2613,10 @@ class MentraLive : SGCManager() {
|
|
|
2450
2613
|
}
|
|
2451
2614
|
}
|
|
2452
2615
|
|
|
2453
|
-
override fun
|
|
2616
|
+
override fun handleReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
|
2617
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2618
|
+
return
|
|
2619
|
+
}
|
|
2454
2620
|
rssiReadInProgress = false
|
|
2455
2621
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2456
2622
|
if (isConnected && bluetoothGatt != null && gatt === bluetoothGatt) {
|
|
@@ -2461,11 +2627,14 @@ class MentraLive : SGCManager() {
|
|
|
2461
2627
|
}
|
|
2462
2628
|
}
|
|
2463
2629
|
|
|
2464
|
-
override fun
|
|
2630
|
+
override fun handleCharacteristicWrite(
|
|
2465
2631
|
gatt: BluetoothGatt,
|
|
2466
2632
|
characteristic: BluetoothGattCharacteristic,
|
|
2467
2633
|
status: Int
|
|
2468
2634
|
) {
|
|
2635
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2636
|
+
return
|
|
2637
|
+
}
|
|
2469
2638
|
val trace = inFlightBleWriteTrace
|
|
2470
2639
|
val callbackAtMs = System.currentTimeMillis()
|
|
2471
2640
|
val callbackDelayMs =
|
|
@@ -2527,11 +2696,12 @@ class MentraLive : SGCManager() {
|
|
|
2527
2696
|
}
|
|
2528
2697
|
}
|
|
2529
2698
|
|
|
2530
|
-
override fun
|
|
2699
|
+
override fun handleCharacteristicChanged(
|
|
2531
2700
|
gatt: BluetoothGatt,
|
|
2532
|
-
characteristic: BluetoothGattCharacteristic
|
|
2701
|
+
characteristic: BluetoothGattCharacteristic,
|
|
2702
|
+
data: ByteArray
|
|
2533
2703
|
) {
|
|
2534
|
-
if (!
|
|
2704
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2535
2705
|
return
|
|
2536
2706
|
}
|
|
2537
2707
|
|
|
@@ -2539,8 +2709,7 @@ class MentraLive : SGCManager() {
|
|
|
2539
2709
|
val threadId = Thread.currentThread().id
|
|
2540
2710
|
val uuid = characteristic.uuid
|
|
2541
2711
|
|
|
2542
|
-
|
|
2543
|
-
if (data == null || data.isEmpty()) {
|
|
2712
|
+
if (data.isEmpty()) {
|
|
2544
2713
|
return
|
|
2545
2714
|
}
|
|
2546
2715
|
|
|
@@ -2584,11 +2753,14 @@ class MentraLive : SGCManager() {
|
|
|
2584
2753
|
processReceivedData(data, data.size)
|
|
2585
2754
|
}
|
|
2586
2755
|
|
|
2587
|
-
override fun
|
|
2756
|
+
override fun handleDescriptorWrite(
|
|
2588
2757
|
gatt: BluetoothGatt,
|
|
2589
2758
|
descriptor: BluetoothGattDescriptor,
|
|
2590
2759
|
status: Int
|
|
2591
2760
|
) {
|
|
2761
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2762
|
+
return
|
|
2763
|
+
}
|
|
2592
2764
|
val threadId = Thread.currentThread().id
|
|
2593
2765
|
|
|
2594
2766
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
@@ -2616,43 +2788,91 @@ class MentraLive : SGCManager() {
|
|
|
2616
2788
|
writeNextDescriptor()
|
|
2617
2789
|
}
|
|
2618
2790
|
|
|
2619
|
-
override fun
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2791
|
+
override fun handleMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
|
2792
|
+
handler.post {
|
|
2793
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2794
|
+
return@post
|
|
2795
|
+
}
|
|
2796
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2797
|
+
Bridge.log(
|
|
2798
|
+
"LIVE: 🔵 MTU negotiation successful - changed to " +
|
|
2799
|
+
mtu +
|
|
2800
|
+
" bytes"
|
|
2801
|
+
)
|
|
2802
|
+
val effectivePayload = mtu - 3
|
|
2803
|
+
Bridge.log(
|
|
2804
|
+
"LIVE: Effective payload size: " +
|
|
2805
|
+
effectivePayload +
|
|
2806
|
+
" bytes"
|
|
2807
|
+
)
|
|
2628
2808
|
|
|
2629
|
-
|
|
2630
|
-
currentMtu = mtu
|
|
2809
|
+
currentMtu = mtu
|
|
2631
2810
|
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2811
|
+
if (mtu >= 64) {
|
|
2812
|
+
Bridge.log(
|
|
2813
|
+
"LIVE: ✅ MTU size is sufficient for LC3 audio data packets"
|
|
2814
|
+
)
|
|
2815
|
+
} else {
|
|
2816
|
+
Log.w(
|
|
2817
|
+
TAG,
|
|
2818
|
+
"⚠️ MTU size may be too small for LC3 audio data packets"
|
|
2819
|
+
)
|
|
2820
|
+
Bridge.log(
|
|
2821
|
+
"LIVE: 📊 Effective MTU payload: " +
|
|
2822
|
+
effectivePayload +
|
|
2823
|
+
" bytes"
|
|
2824
|
+
)
|
|
2825
|
+
}
|
|
2636
2826
|
} else {
|
|
2637
|
-
Log.
|
|
2638
|
-
|
|
2639
|
-
|
|
2827
|
+
Log.e(TAG, "❌ MTU change failed with status: " + status)
|
|
2828
|
+
Log.w(
|
|
2829
|
+
TAG,
|
|
2830
|
+
" Will continue with default MTU (23 bytes, 20 byte payload)"
|
|
2640
2831
|
)
|
|
2641
2832
|
}
|
|
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
2833
|
|
|
2647
|
-
|
|
2648
|
-
// (descriptor writes are GATT operations and can't overlap with MTU request)
|
|
2649
|
-
if (!notificationsEnabled) {
|
|
2650
|
-
notificationsEnabled = true
|
|
2651
|
-
enableNotifications()
|
|
2834
|
+
mtuSetupToken?.let { completeMtuSetup(gatt, it, "callback") }
|
|
2652
2835
|
}
|
|
2653
2836
|
}
|
|
2654
2837
|
}
|
|
2655
2838
|
|
|
2839
|
+
private fun startMtuWatchdog(gatt: BluetoothGatt, token: Long) {
|
|
2840
|
+
mtuWatchdogRunnable?.let { gattLifecycleHandler.removeCallbacks(it) }
|
|
2841
|
+
val watchdog =
|
|
2842
|
+
Runnable {
|
|
2843
|
+
handler.post {
|
|
2844
|
+
if (!isActiveGattCallback(gatt)) {
|
|
2845
|
+
return@post
|
|
2846
|
+
}
|
|
2847
|
+
Bridge.log(
|
|
2848
|
+
"LIVE: ⚠️ MTU callback timed out after ${MTU_CALLBACK_TIMEOUT_MS}ms; continuing with notification setup"
|
|
2849
|
+
)
|
|
2850
|
+
completeMtuSetup(gatt, token, "watchdog")
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
mtuWatchdogRunnable = watchdog
|
|
2854
|
+
gattLifecycleHandler.postDelayed(watchdog, MTU_CALLBACK_TIMEOUT_MS)
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
private fun completeMtuSetup(gatt: BluetoothGatt, token: Long, reason: String) {
|
|
2858
|
+
if (!mtuSetupGate.complete(token)) {
|
|
2859
|
+
return
|
|
2860
|
+
}
|
|
2861
|
+
mtuWatchdogRunnable?.let { gattLifecycleHandler.removeCallbacks(it) }
|
|
2862
|
+
mtuWatchdogRunnable = null
|
|
2863
|
+
mtuSetupToken = null
|
|
2864
|
+
enableNotificationsAfterMtu(gatt, reason)
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
private fun enableNotificationsAfterMtu(gatt: BluetoothGatt, reason: String) {
|
|
2868
|
+
if (!isActiveGattCallback(gatt) || notificationsEnabled) {
|
|
2869
|
+
return
|
|
2870
|
+
}
|
|
2871
|
+
notificationsEnabled = true
|
|
2872
|
+
Bridge.log("LIVE: 🔔 Continuing GATT setup after MTU ($reason)")
|
|
2873
|
+
enableNotifications()
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2656
2876
|
/**
|
|
2657
2877
|
* Write the next queued descriptor, or mark the queue as idle. Must be called after each
|
|
2658
2878
|
* onDescriptorWrite callback to serialize BLE GATT operations. On older Android devices,
|
|
@@ -2660,6 +2880,12 @@ class MentraLive : SGCManager() {
|
|
|
2660
2880
|
* subsequent writes to silently fail.
|
|
2661
2881
|
*/
|
|
2662
2882
|
private fun writeNextDescriptor() {
|
|
2883
|
+
val gatt = bluetoothGatt ?: return
|
|
2884
|
+
if (!isActiveGattCallback(gatt)) return
|
|
2885
|
+
val epoch = gattEpoch
|
|
2886
|
+
val continueSetup = Runnable {
|
|
2887
|
+
if (epoch == gattEpoch && isActiveGattCallback(gatt)) writeNextDescriptor()
|
|
2888
|
+
}
|
|
2663
2889
|
val next = pendingDescriptorWrites.poll()
|
|
2664
2890
|
if (next == null) {
|
|
2665
2891
|
isDescriptorWriteInProgress = false
|
|
@@ -2703,12 +2929,12 @@ class MentraLive : SGCManager() {
|
|
|
2703
2929
|
uuid +
|
|
2704
2930
|
", continuing queue"
|
|
2705
2931
|
)
|
|
2706
|
-
handler.postDelayed(
|
|
2932
|
+
handler.postDelayed(continueSetup, 50L)
|
|
2707
2933
|
}
|
|
2708
2934
|
} catch (e: Exception) {
|
|
2709
2935
|
val threadId = Thread.currentThread().id
|
|
2710
2936
|
Log.e(TAG, "Thread-" + threadId + ": ⚠️ Error writing descriptor: " + e.message)
|
|
2711
|
-
handler.postDelayed(
|
|
2937
|
+
handler.postDelayed(continueSetup, 50L)
|
|
2712
2938
|
}
|
|
2713
2939
|
}
|
|
2714
2940
|
|
|
@@ -4743,7 +4969,7 @@ class MentraLive : SGCManager() {
|
|
|
4743
4969
|
handleGlassesSessionId(json)
|
|
4744
4970
|
|
|
4745
4971
|
Bridge.log("LIVE: Processed version_info fields and sent to RN")
|
|
4746
|
-
Bridge.sendVersionInfo(fields)
|
|
4972
|
+
Bridge.sendVersionInfo(fields, type)
|
|
4747
4973
|
} else {
|
|
4748
4974
|
Log.d(TAG, "📦 Unknown message type: " + type)
|
|
4749
4975
|
}
|
|
@@ -5664,9 +5890,14 @@ class MentraLive : SGCManager() {
|
|
|
5664
5890
|
* build number, firmware version, etc.
|
|
5665
5891
|
*/
|
|
5666
5892
|
override fun requestVersionInfo() {
|
|
5893
|
+
requestVersionInfo(null)
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5896
|
+
fun requestVersionInfo(requestId: String?) {
|
|
5667
5897
|
try {
|
|
5668
5898
|
val json = JSONObject()
|
|
5669
5899
|
json.put("type", "request_version")
|
|
5900
|
+
requestId?.let { json.put("request_id", it) }
|
|
5670
5901
|
sendJson(json, false)
|
|
5671
5902
|
Bridge.log("LIVE: 📱 Requesting version info from glasses")
|
|
5672
5903
|
} catch (e: JSONException) {
|
|
@@ -5938,6 +6169,8 @@ class MentraLive : SGCManager() {
|
|
|
5938
6169
|
// SmartGlassesCommunicator interface implementation
|
|
5939
6170
|
|
|
5940
6171
|
override fun findCompatibleDevices() {
|
|
6172
|
+
if (postGattLifecycle { findCompatibleDevices() }) return
|
|
6173
|
+
connectionRequestEpoch++
|
|
5941
6174
|
Bridge.log("LIVE: Finding compatible Mentra Live glasses")
|
|
5942
6175
|
|
|
5943
6176
|
// A fresh user scan owns the radio. Invalidate delayed reconnect callbacks
|
|
@@ -6002,6 +6235,7 @@ class MentraLive : SGCManager() {
|
|
|
6002
6235
|
}
|
|
6003
6236
|
|
|
6004
6237
|
override fun connectById(id: String) {
|
|
6238
|
+
if (postGattLifecycle { connectById(id) }) return
|
|
6005
6239
|
if (pairingYieldActive) {
|
|
6006
6240
|
Bridge.log("LIVE: connectById blocked — pairing yield active")
|
|
6007
6241
|
return
|
|
@@ -6031,6 +6265,7 @@ class MentraLive : SGCManager() {
|
|
|
6031
6265
|
}
|
|
6032
6266
|
|
|
6033
6267
|
override fun forget() {
|
|
6268
|
+
if (postGattLifecycle { forget() }) return
|
|
6034
6269
|
Bridge.log("LIVE: Forgetting Mentra Live glasses")
|
|
6035
6270
|
|
|
6036
6271
|
// Clear saved device name so a leftover name can't bypass the pairing-mode
|
|
@@ -6075,6 +6310,7 @@ class MentraLive : SGCManager() {
|
|
|
6075
6310
|
}
|
|
6076
6311
|
|
|
6077
6312
|
override fun disconnect() {
|
|
6313
|
+
if (postGattLifecycle { disconnect() }) return
|
|
6078
6314
|
if (unpairFlushPending) {
|
|
6079
6315
|
Bridge.log("LIVE: disconnect deferred — waiting for unpair write")
|
|
6080
6316
|
return
|
|
@@ -6116,6 +6352,7 @@ class MentraLive : SGCManager() {
|
|
|
6116
6352
|
}
|
|
6117
6353
|
|
|
6118
6354
|
fun connectToSmartGlasses() {
|
|
6355
|
+
if (postGattLifecycle { connectToSmartGlasses() }) return
|
|
6119
6356
|
if (pairingYieldActive) {
|
|
6120
6357
|
Bridge.log("LIVE: connectToSmartGlasses blocked — pairing yield active")
|
|
6121
6358
|
return
|
|
@@ -7247,6 +7484,8 @@ class MentraLive : SGCManager() {
|
|
|
7247
7484
|
}
|
|
7248
7485
|
|
|
7249
7486
|
fun destroy() {
|
|
7487
|
+
if (postGattLifecycle { destroy() }) return
|
|
7488
|
+
connectionRequestEpoch++
|
|
7250
7489
|
Bridge.log("LIVE: Destroying MentraLiveSGC")
|
|
7251
7490
|
|
|
7252
7491
|
// Mark as killed to prevent reconnection attempts
|