From c041792e3eb6ce989f121395eca2c512ac3e3c11 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 02:52:33 +0000 Subject: [PATCH 1/4] feat(connections): Show progress indicator during connection This commit introduces a progress indicator that is displayed on the `ConnectionsScreen` while the application is connecting to a Bluetooth device. - A `CONNECTING` state has been added to the `ConnectionState` enum. - A `LinearProgressIndicator` is now shown on the `ConnectionsScreen` when the state is `CONNECTING`. - A `connectionProgress` flow has been added to the `ServiceRepository` to broadcast the connection progress. - The `MeshService` has been updated to emit progress updates on the new flow during the connection process. - The `ConnectionsViewModel` has been updated to use the new progress flow. - The state management bug in `MeshService` has been fixed to ensure the `CONNECTING` state is set correctly. --- .../com/geeksville/mesh/service/MeshService.kt | 13 +++++++++++-- .../mesh/ui/connections/ConnectionsScreen.kt | 17 ++++++++++++++++- .../mesh/ui/connections/ConnectionsViewModel.kt | 15 +++++++++++++++ .../meshtastic/core/service/ConnectionState.kt | 3 +++ .../core/service/ServiceRepository.kt | 8 ++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) 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..9d8e1573b4 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1273,6 +1273,7 @@ class MeshService : Service() { fun startConnect() { // Do our startup init try { + onConnectionChanged(ConnectionState.CONNECTING) connectTimeMsec = System.currentTimeMillis() startConfig() } catch (ex: InvalidProtocolBufferException) { @@ -1304,6 +1305,7 @@ class MeshService : Service() { connectionStateHolder.setState(c) when (c) { ConnectionState.CONNECTED -> startConnect() + ConnectionState.CONNECTING -> {} ConnectionState.DEVICE_SLEEP -> startDeviceSleep() ConnectionState.DISCONNECTED -> startDisconnect() } @@ -1315,7 +1317,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) } @@ -1342,6 +1344,7 @@ class MeshService : Service() { } ConnectionState.DISCONNECTED -> ConnectionState.DISCONNECTED + else -> newState } onConnectionChanged(effectiveState) } @@ -1431,6 +1434,7 @@ class MeshService : Service() { setLocalConfig(config) val configCount = localConfig.allFields.size serviceRepository.setStatusMessage("Device config ($configCount / $configTotal)") + serviceRepository.setConnectionProgress(configCount.toFloat() / configTotal.toFloat()) } private fun handleModuleConfig(config: ModuleConfigProtos.ModuleConfig) { @@ -1447,6 +1451,7 @@ class MeshService : Service() { setLocalModuleConfig(config) val moduleCount = moduleConfig.allFields.size serviceRepository.setStatusMessage("Module config ($moduleCount / $moduleTotal)") + serviceRepository.setConnectionProgress(moduleCount.toFloat() / moduleTotal.toFloat()) } private fun handleChannel(ch: ChannelProtos.Channel) { @@ -1463,6 +1468,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)") + serviceRepository.setConnectionProgress((ch.index + 1).toFloat() / maxChannels.toFloat()) } /** Convert a protobuf NodeInfo into our model objects and update our node DB */ @@ -1507,6 +1513,7 @@ class MeshService : Service() { } } + private var nodesTotal = 0 private fun handleNodeInfo(info: MeshProtos.NodeInfo) { Timber.d( "Received nodeinfo num=${info.num}," + @@ -1526,7 +1533,8 @@ class MeshService : Service() { insertMeshLog(packetToSave) newNodes.add(info) - serviceRepository.setStatusMessage("Nodes (${newNodes.size})") + serviceRepository.setStatusMessage("Nodes (${newNodes.size} / $nodesTotal)") + serviceRepository.setConnectionProgress(newNodes.size.toFloat() / nodesTotal.toFloat()) } private var rawMyNodeInfo: MeshProtos.MyNodeInfo? = null @@ -1591,6 +1599,7 @@ class MeshService : Service() { rawMyNodeInfo = myInfo regenMyNodeInfo() + nodesTotal = myInfo.numNodes // We'll need to get a new set of channels and settings now serviceScope.handledLaunch { 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 From efdc3e3f14373484c35737315253febcd4e3ff42 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 03:24:30 +0000 Subject: [PATCH 2/4] feat(connections): Show progress indicator during connection This commit introduces a progress indicator that is displayed on the `ConnectionsScreen` while the application is connecting to a Bluetooth device. - A `CONNECTING` state has been added to the `ConnectionState` enum. - A `LinearProgressIndicator` is now shown on the `ConnectionsScreen` when the state is `CONNECTING`. - A `connectionProgress` flow has been added to the `ServiceRepository` to broadcast the connection progress. - The `MeshService` has been updated to emit progress updates on the new flow during the connection process. - The `ConnectionsViewModel` has been updated to use the new progress flow. - The state management bug in `MeshService` has been fixed to ensure the `CONNECTING` state is set correctly. From 35d34cdd046cdf1a615e71a22687fedafa97bfe8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 03:39:44 +0000 Subject: [PATCH 3/4] feat(connections): Show progress indicator during connection This commit introduces a progress indicator that is displayed on the `ConnectionsScreen` while the application is connecting to a Bluetooth device. - A `CONNECTING` state has been added to the `ConnectionState` enum. - A `LinearProgressIndicator` is now shown on the `ConnectionsScreen` when the state is `CONNECTING`. - A `connectionProgress` flow has been added to the `ServiceRepository` to broadcast the connection progress. - The `MeshService` has been updated to emit progress updates on the new flow during the connection process. - The `ConnectionsViewModel` has been updated to use the new progress flow. - The state management bug in `MeshService` has been fixed to ensure the `CONNECTING` state is set correctly. --- app/src/main/java/com/geeksville/mesh/service/MeshService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9d8e1573b4..d2b3266d0f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1599,7 +1599,7 @@ class MeshService : Service() { rawMyNodeInfo = myInfo regenMyNodeInfo() - nodesTotal = myInfo.numNodes + nodesTotal = myInfo.nodedbCount // We'll need to get a new set of channels and settings now serviceScope.handledLaunch { From 90eb0a9663fda4301e38866c97e6718d021abe7a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 04:26:08 +0000 Subject: [PATCH 4/4] feat: Show connection progress indicator Adds a linear progress indicator to the connections screen to show the user the progress of the bluetooth connection. The progress is weighted based on the different stages of the connection process: - Device config - Module config - Channels - Node database A 30-second timeout has also been added to the connection process. --- .../geeksville/mesh/service/MeshService.kt | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) 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 d2b3266d0f..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 @@ -1273,9 +1274,13 @@ class MeshService : Service() { fun startConnect() { // Do our startup init try { - onConnectionChanged(ConnectionState.CONNECTING) 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) { @@ -1297,15 +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.CONNECTING -> {} + ConnectionState.CONNECTED -> onHasSettings() + ConnectionState.CONNECTING -> startConnect() ConnectionState.DEVICE_SLEEP -> startDeviceSleep() ConnectionState.DISCONNECTED -> startDisconnect() } @@ -1335,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 @@ -1420,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 = @@ -1434,7 +1445,7 @@ class MeshService : Service() { setLocalConfig(config) val configCount = localConfig.allFields.size serviceRepository.setStatusMessage("Device config ($configCount / $configTotal)") - serviceRepository.setConnectionProgress(configCount.toFloat() / configTotal.toFloat()) + setProgress(0, configCount.toFloat() / configTotal.toFloat()) } private fun handleModuleConfig(config: ModuleConfigProtos.ModuleConfig) { @@ -1451,7 +1462,7 @@ class MeshService : Service() { setLocalModuleConfig(config) val moduleCount = moduleConfig.allFields.size serviceRepository.setStatusMessage("Module config ($moduleCount / $moduleTotal)") - serviceRepository.setConnectionProgress(moduleCount.toFloat() / moduleTotal.toFloat()) + setProgress(1, moduleCount.toFloat() / moduleTotal.toFloat()) } private fun handleChannel(ch: ChannelProtos.Channel) { @@ -1468,7 +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)") - serviceRepository.setConnectionProgress((ch.index + 1).toFloat() / maxChannels.toFloat()) + setProgress(2, (ch.index + 1).toFloat() / maxChannels.toFloat()) } /** Convert a protobuf NodeInfo into our model objects and update our node DB */ @@ -1534,7 +1545,9 @@ class MeshService : Service() { newNodes.add(info) serviceRepository.setStatusMessage("Nodes (${newNodes.size} / $nodesTotal)") - serviceRepository.setConnectionProgress(newNodes.size.toFloat() / nodesTotal.toFloat()) + if (nodesTotal > 0) { + setProgress(3, newNodes.size.toFloat() / nodesTotal.toFloat()) + } } private var rawMyNodeInfo: MeshProtos.MyNodeInfo? = null @@ -1599,7 +1612,10 @@ class MeshService : Service() { rawMyNodeInfo = myInfo regenMyNodeInfo() - nodesTotal = myInfo.nodedbCount + nodesTotal = myInfo.nodedbCount + if (nodesTotal == 0) { + setProgress(3, 1f) + } // We'll need to get a new set of channels and settings now serviceScope.handledLaunch { @@ -1782,7 +1798,7 @@ class MeshService : Service() { haveNodeDB = true // we now have nodes from real hardware sendAnalytics() - onHasSettings() + onConnectionChanged(ConnectionState.CONNECTED) } }