diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt index 9721e5661..0834c4e4d 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt @@ -158,14 +158,18 @@ constructor( @Suppress("LoopWithTooManyJumpStatements", "MagicNumber") val pollingJob = service.serviceScope.handledLaunch { - while (true) { - try { - delay(2500) // Poll every 5 seconds - safe?.asyncReadRemoteRssi { res -> res.getOrNull()?.let { trySend(it) } } - } catch (ex: CancellationException) { - break // Stop polling on cancellation - } catch (ex: Exception) { - Timber.d("RSSI polling error: ${ex.message}") + service.isRssiPollingEnabled.collect { isEnabled -> + if (isEnabled) { + while (true) { + try { + delay(10000) // Poll every 10 seconds + safe?.asyncReadRemoteRssi { res -> res.getOrNull()?.let { trySend(it) } } + } catch (ex: CancellationException) { + break // Stop polling on cancellation + } catch (ex: Exception) { + Timber.d("RSSI polling error: ${ex.message}") + } + } } } } diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt index 7765cf514..3ee7e26c7 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt @@ -87,6 +87,13 @@ constructor( private val _currentDeviceAddressFlow = MutableStateFlow(radioPrefs.devAddr) val currentDeviceAddressFlow: StateFlow = _currentDeviceAddressFlow.asStateFlow() + private val _isRssiPollingEnabled = MutableStateFlow(false) + val isRssiPollingEnabled: StateFlow = _isRssiPollingEnabled.asStateFlow() + + fun setRssiPolling(enabled: Boolean) { + _isRssiPollingEnabled.value = enabled + } + private val logSends = false private val logReceives = false private lateinit var sentPacketsLog: BinaryLogFile diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetoothGattCallback.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetoothGattCallback.kt index 52cd1f809..7a42f916b 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetoothGattCallback.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetoothGattCallback.kt @@ -17,12 +17,14 @@ package com.geeksville.mesh.service +import android.Manifest import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothProfile import android.os.Build +import androidx.annotation.RequiresPermission import com.geeksville.mesh.logAssert import com.geeksville.mesh.util.exceptionReporter import timber.log.Timber @@ -43,20 +45,20 @@ internal class SafeBluetoothGattCallback(private val safeBluetooth: SafeBluetoot private const val MYSTERY_STATUS_CODE = 257 } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @Suppress("CyclomaticComplexMethod") override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) = exceptionReporter { Timber.i("new bluetooth connection state $newState, status $status") when (newState) { BluetoothProfile.STATE_CONNECTED -> { - safeBluetooth.state = - newState // we only care about connected/disconnected - not the transitional states - // If autoconnect is on and this connect attempt failed, hopefully some future attempt will // succeed - if (status != BluetoothGatt.GATT_SUCCESS && safeBluetooth.autoReconnect) { - Timber.e("Connect attempt failed $status, not calling connect completion handler...") + if (status != BluetoothGatt.GATT_SUCCESS) { + Timber.e("Connect attempt failed with status $status") + safeBluetooth.lostConnection("connection failed with status $status") } else { + safeBluetooth.state = newState workQueue.completeWork(status, Unit) } } @@ -111,6 +113,10 @@ internal class SafeBluetoothGattCallback(private val safeBluetooth: SafeBluetoot } } } + else -> { + // Anything that is not a successful connection should be treated as a failure. + safeBluetooth.lostConnection("unexpected connection state: $newState") + } } } @@ -142,6 +148,7 @@ internal class SafeBluetoothGattCallback(private val safeBluetooth: SafeBluetoot workQueue.completeWork(status, Unit) } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { val reliable = safeBluetooth.currentReliableWrite if (reliable != null) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt index dcd4b3301..fae9e9ca7 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -116,6 +117,11 @@ fun ConnectionsScreen( val recentTcpDevices by scanModel.recentTcpDevicesForUi.collectAsStateWithLifecycle() val usbDevices by scanModel.usbDevicesForUi.collectAsStateWithLifecycle() + DisposableEffect(Unit) { + connectionsViewModel.onStart() + onDispose { connectionsViewModel.onStop() } + } + /* Animate waiting for the configurations */ var isWaiting by remember { mutableStateOf(false) } if (isWaiting) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt index 1990fa6a0..3594e53e3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt @@ -19,6 +19,7 @@ package com.geeksville.mesh.ui.connections import androidx.lifecycle.ViewModel import com.geeksville.mesh.repository.bluetooth.BluetoothRepository +import com.geeksville.mesh.repository.radio.RadioInterfaceService import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -39,10 +40,19 @@ class ConnectionsViewModel constructor( radioConfigRepository: RadioConfigRepository, serviceRepository: ServiceRepository, + private val radioInterfaceService: RadioInterfaceService, nodeRepository: NodeRepository, bluetoothRepository: BluetoothRepository, private val uiPrefs: UiPrefs, ) : ViewModel() { + fun onStart() { + radioInterfaceService.setRssiPolling(true) + } + + fun onStop() { + radioInterfaceService.setRssiPolling(false) + } + val localConfig: StateFlow = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance())