diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index f3fb2b45f9..c3590a81b7 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1216,6 +1216,7 @@ class MeshService : Service() { } private var sleepTimeout: Job? = null + private var connectionTimeoutJob: Job? = null // msecs since 1970 we started this connection private var connectTimeMsec = 0L @@ -1275,6 +1276,11 @@ class MeshService : Service() { try { connectTimeMsec = System.currentTimeMillis() startConfig() + connectionTimeoutJob = serviceScope.handledLaunch { + delay(30000) // 30-second timeout + Timber.w("Connection timeout reached") + onConnectionChanged(ConnectionState.DISCONNECTED) + } } catch (ex: InvalidProtocolBufferException) { Timber.e(ex, "Invalid protocol buffer sent by device - update device software and try again") } catch (ex: RadioNotConnectedException) { @@ -1296,14 +1302,16 @@ class MeshService : Service() { } // Cancel any existing timeouts - sleepTimeout?.let { - it.cancel() - sleepTimeout = null - } + sleepTimeout?.cancel() + sleepTimeout = null + + connectionTimeoutJob?.cancel() + connectionTimeoutJob = null connectionStateHolder.setState(c) when (c) { - ConnectionState.CONNECTED -> startConnect() + ConnectionState.CONNECTED -> onHasSettings() + ConnectionState.CONNECTING -> startConnect() ConnectionState.DEVICE_SLEEP -> startDeviceSleep() ConnectionState.DISCONNECTED -> startDisconnect() } @@ -1315,7 +1323,7 @@ class MeshService : Service() { val notificationSummary = when (connectionStateHolder.getState()) { ConnectionState.CONNECTED -> getString(R.string.connected_count).format(numOnlineNodes) - + ConnectionState.CONNECTING -> getString(R.string.connecting_to_device) ConnectionState.DISCONNECTED -> getString(R.string.disconnected) ConnectionState.DEVICE_SLEEP -> getString(R.string.device_sleeping) } @@ -1333,7 +1341,7 @@ class MeshService : Service() { val effectiveState = when (newState) { - ConnectionState.CONNECTED -> ConnectionState.CONNECTED + ConnectionState.CONNECTED -> if (connectionStateHolder.getState() == ConnectionState.DISCONNECTED) ConnectionState.CONNECTING else ConnectionState.CONNECTED ConnectionState.DEVICE_SLEEP -> if (lsEnabled) { ConnectionState.DEVICE_SLEEP @@ -1342,6 +1350,7 @@ class MeshService : Service() { } ConnectionState.DISCONNECTED -> ConnectionState.DISCONNECTED + else -> newState } onConnectionChanged(effectiveState) } @@ -1417,6 +1426,11 @@ class MeshService : Service() { // provisional NodeInfos we will install if all goes well private val newNodes = mutableListOf() + private val progressWeights = floatArrayOf(0.1f, 0.1f, 0.2f, 0.6f) + private fun setProgress(step: Int, progress: Float) { + val totalProgress = progressWeights.take(step).sum() + progress * progressWeights[step] + serviceRepository.setConnectionProgress(totalProgress) + } private fun handleDeviceConfig(config: ConfigProtos.Config) { Timber.d("Received config ${config.toOneLineString()}") val packetToSave = @@ -1431,6 +1445,7 @@ class MeshService : Service() { setLocalConfig(config) val configCount = localConfig.allFields.size serviceRepository.setStatusMessage("Device config ($configCount / $configTotal)") + setProgress(0, configCount.toFloat() / configTotal.toFloat()) } private fun handleModuleConfig(config: ModuleConfigProtos.ModuleConfig) { @@ -1447,6 +1462,7 @@ class MeshService : Service() { setLocalModuleConfig(config) val moduleCount = moduleConfig.allFields.size serviceRepository.setStatusMessage("Module config ($moduleCount / $moduleTotal)") + setProgress(1, moduleCount.toFloat() / moduleTotal.toFloat()) } private fun handleChannel(ch: ChannelProtos.Channel) { @@ -1463,6 +1479,7 @@ class MeshService : Service() { if (ch.role != ChannelProtos.Channel.Role.DISABLED) updateChannelSettings(ch) val maxChannels = myNodeInfo?.maxChannels ?: 8 serviceRepository.setStatusMessage("Channels (${ch.index + 1} / $maxChannels)") + setProgress(2, (ch.index + 1).toFloat() / maxChannels.toFloat()) } /** Convert a protobuf NodeInfo into our model objects and update our node DB */ @@ -1507,6 +1524,7 @@ class MeshService : Service() { } } + private var nodesTotal = 0 private fun handleNodeInfo(info: MeshProtos.NodeInfo) { Timber.d( "Received nodeinfo num=${info.num}," + @@ -1526,7 +1544,10 @@ class MeshService : Service() { insertMeshLog(packetToSave) newNodes.add(info) - serviceRepository.setStatusMessage("Nodes (${newNodes.size})") + serviceRepository.setStatusMessage("Nodes (${newNodes.size} / $nodesTotal)") + if (nodesTotal > 0) { + setProgress(3, newNodes.size.toFloat() / nodesTotal.toFloat()) + } } private var rawMyNodeInfo: MeshProtos.MyNodeInfo? = null @@ -1591,6 +1612,10 @@ class MeshService : Service() { rawMyNodeInfo = myInfo regenMyNodeInfo() + nodesTotal = myInfo.nodedbCount + if (nodesTotal == 0) { + setProgress(3, 1f) + } // We'll need to get a new set of channels and settings now serviceScope.handledLaunch { @@ -1773,7 +1798,7 @@ class MeshService : Service() { haveNodeDB = true // we now have nodes from real hardware sendAnalytics() - onHasSettings() + onConnectionChanged(ConnectionState.CONNECTED) } } 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 fae9e9ca7f..5fb2660608 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 @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -34,6 +35,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Language +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -156,7 +158,7 @@ fun ConnectionsScreen( ConnectionState.CONNECTED -> { if (regionUnset) R.string.must_set_region else R.string.connected } - + ConnectionState.CONNECTING -> R.string.connecting_to_device ConnectionState.DISCONNECTED -> R.string.not_connected ConnectionState.DEVICE_SLEEP -> R.string.connected_sleeping }.let { scanModel.setErrorText(context.getString(it)) } @@ -185,6 +187,19 @@ fun ConnectionsScreen( .padding(paddingValues) .padding(16.dp), ) { + AnimatedVisibility( + visible = connectionState == ConnectionState.CONNECTING, + modifier = Modifier.padding(bottom = 16.dp), + ) { + val connectionProgress by connectionsViewModel.connectionProgress.collectAsStateWithLifecycle() + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(text = stringResource(id = R.string.connecting_to_device)) + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + progress = { connectionProgress }, + ) + } + } AnimatedVisibility( visible = connectionState.isConnected(), modifier = Modifier.padding(bottom = 16.dp), 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 3594e53e34..b6c8599477 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 @@ -18,17 +18,22 @@ package com.geeksville.mesh.ui.connections import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.geeksville.mesh.repository.bluetooth.BluetoothRepository import com.geeksville.mesh.repository.radio.RadioInterfaceService import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.database.model.Node import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.service.ConnectionState import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalOnlyProtos.LocalConfig @@ -45,6 +50,16 @@ constructor( bluetoothRepository: BluetoothRepository, private val uiPrefs: UiPrefs, ) : ViewModel() { + + private val _connectionProgress = MutableStateFlow(0f) + val connectionProgress = _connectionProgress.asStateFlow() + + init { + serviceRepository.connectionProgress.onEach { + _connectionProgress.value = it + }.launchIn(viewModelScope) + } + fun onStart() { radioInterfaceService.setRssiPolling(true) } diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/ConnectionState.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/ConnectionState.kt index 394c760dac..918d536322 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/ConnectionState.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/ConnectionState.kt @@ -21,6 +21,9 @@ enum class ConnectionState { /** We are disconnected from the device, and we should be trying to reconnect. */ DISCONNECTED, + /** We are in the process of connecting to a device */ + CONNECTING, + /** We are connected to the device and communicating normally. */ CONNECTED, diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt index 1adad9bf90..cc1ef16e49 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt @@ -95,6 +95,14 @@ class ServiceRepository @Inject constructor() { } } + private val _connectionProgress = MutableStateFlow(0f) + val connectionProgress: StateFlow + get() = _connectionProgress + + fun setConnectionProgress(progress: Float) { + _connectionProgress.value = progress + } + private val _meshPacketFlow = MutableSharedFlow() val meshPacketFlow: SharedFlow get() = _meshPacketFlow