mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(service): unify dual connectionState flows into single source of truth (#5077)
This commit is contained in:
parent
5e44cbd3a9
commit
9468bc6ebe
12 changed files with 103 additions and 8 deletions
|
|
@ -98,6 +98,9 @@ class MeshConnectionManagerImpl(
|
|||
private var connectionRestored = false
|
||||
|
||||
init {
|
||||
// Bridge transport-level state into the canonical app-level state.
|
||||
// This is the ONLY consumer of RadioInterfaceService.connectionState — it applies
|
||||
// light-sleep policy and handshake awareness before writing to ServiceRepository.
|
||||
radioInterfaceService.connectionState.onEach(::onRadioConnectionState).launchIn(scope)
|
||||
|
||||
// Ensure notification title and content stay in sync with state changes
|
||||
|
|
@ -131,6 +134,13 @@ class MeshConnectionManagerImpl(
|
|||
.launchIn(scope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridges a transport-level [ConnectionState] into the canonical app-level state.
|
||||
*
|
||||
* Applies light-sleep policy (power-saving / router role) to decide whether a [ConnectionState.DeviceSleep] event
|
||||
* should be surfaced as sleep or as a full disconnect, then delegates to [onConnectionChanged] for the actual state
|
||||
* transition.
|
||||
*/
|
||||
private suspend fun onRadioConnectionState(newState: ConnectionState) {
|
||||
val localConfig = radioConfigRepository.localConfigFlow.first()
|
||||
val isRouter = localConfig.device?.role == Config.DeviceConfig.Role.ROUTER
|
||||
|
|
|
|||
|
|
@ -28,7 +28,16 @@ import org.meshtastic.proto.ClientNotification
|
|||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface RadioController {
|
||||
/** Reactive connection state of the radio. */
|
||||
/**
|
||||
* Canonical app-level connection state, delegated from [ServiceRepository][connectionState].
|
||||
*
|
||||
* This exposes the same single source of truth as `ServiceRepository.connectionState`, surfaced through the
|
||||
* controller interface for convenience in feature modules and ViewModels that depend on [RadioController] rather
|
||||
* than [ServiceRepository] directly.
|
||||
*
|
||||
* This is **not** the transport-level state — it reflects the fully reconciled app-level state including handshake
|
||||
* progress and device sleep policy.
|
||||
*/
|
||||
val connectionState: StateFlow<ConnectionState>
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -24,12 +24,42 @@ import org.meshtastic.core.model.DeviceType
|
|||
import org.meshtastic.core.model.InterfaceId
|
||||
import org.meshtastic.core.model.MeshActivity
|
||||
|
||||
/** Interface for the low-level radio interface that handles raw byte communication. */
|
||||
/**
|
||||
* Interface for the low-level radio interface that handles raw byte communication.
|
||||
*
|
||||
* This is the **transport layer** — it manages the raw hardware connection (BLE, TCP, Serial, USB) to a Meshtastic
|
||||
* radio. Its [connectionState] reflects whether the physical link is up or down, **before** any handshake or
|
||||
* config-loading logic is applied.
|
||||
*
|
||||
* **Important:** UI and feature modules should **never** observe [connectionState] directly. Instead, they should use
|
||||
* [ServiceRepository.connectionState], which is the canonical app-level connection state that accounts for handshake
|
||||
* progress, light-sleep policy, and other higher-level concerns. The only legitimate consumer of this transport-level
|
||||
* flow is [MeshConnectionManager], which bridges transport state changes into the app-level
|
||||
* [ServiceRepository.connectionState].
|
||||
*
|
||||
* @see ServiceRepository.connectionState
|
||||
*/
|
||||
interface RadioInterfaceService {
|
||||
/** The device types supported by this platform's radio interface. */
|
||||
val supportedDeviceTypes: List<DeviceType>
|
||||
|
||||
/** Reactive connection state of the radio. */
|
||||
/**
|
||||
* Transport-level connection state of the radio hardware.
|
||||
*
|
||||
* This flow reflects the raw state of the physical link (BLE, TCP, Serial, USB):
|
||||
* - [ConnectionState.Connected] — the transport link is established
|
||||
* - [ConnectionState.Disconnected] — the transport link is down (permanent)
|
||||
* - [ConnectionState.DeviceSleep] — the transport link is down (transient, device sleeping)
|
||||
*
|
||||
* **This is NOT the canonical app-level connection state.** The transport may report [ConnectionState.Connected]
|
||||
* while the app is still performing the mesh handshake (config + node-info exchange), during which the app-level
|
||||
* state remains [ConnectionState.Connecting].
|
||||
*
|
||||
* Only [MeshConnectionManager] should observe this flow. All other consumers (ViewModels, feature modules, UI) must
|
||||
* use [ServiceRepository.connectionState].
|
||||
*
|
||||
* @see ServiceRepository.connectionState
|
||||
*/
|
||||
val connectionState: StateFlow<ConnectionState>
|
||||
|
||||
/** Flow of the current device address. */
|
||||
|
|
|
|||
|
|
@ -31,14 +31,39 @@ import org.meshtastic.proto.MeshPacket
|
|||
*
|
||||
* This repository acts as the primary data bridge between the long-running mesh service and the UI/Feature layers. It
|
||||
* maintains reactive flows for connection status, error messages, and incoming mesh traffic.
|
||||
*
|
||||
* **Connection state contract:** [connectionState] is the **canonical, app-level** connection state that all UI,
|
||||
* feature modules, and ViewModels should observe. It incorporates handshake progress, light-sleep policy, and transport
|
||||
* reconciliation — unlike [RadioInterfaceService.connectionState], which only reflects the raw hardware link status.
|
||||
* The [MeshConnectionManager] is the sole writer of this state; it bridges [RadioInterfaceService.connectionState]
|
||||
* changes into app-level transitions via [setConnectionState].
|
||||
*
|
||||
* @see RadioInterfaceService.connectionState
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface ServiceRepository {
|
||||
/** Reactive flow of the current connection state. */
|
||||
/**
|
||||
* Canonical app-level connection state.
|
||||
*
|
||||
* This is the **single source of truth** for connection status across the entire application. All UI components,
|
||||
* feature modules, and ViewModels should observe this flow — never [RadioInterfaceService.connectionState].
|
||||
*
|
||||
* State transitions are managed exclusively by [MeshConnectionManager], which reconciles transport-level events
|
||||
* with handshake progress and device sleep policy:
|
||||
* - [ConnectionState.Disconnected] — no active connection to a radio
|
||||
* - [ConnectionState.Connecting] — transport is up, mesh handshake (config + node-info) in progress
|
||||
* - [ConnectionState.Connected] — handshake complete, radio fully operational
|
||||
* - [ConnectionState.DeviceSleep] — radio entered light-sleep (transient disconnect)
|
||||
*
|
||||
* @see RadioInterfaceService.connectionState
|
||||
*/
|
||||
val connectionState: StateFlow<ConnectionState>
|
||||
|
||||
/**
|
||||
* Updates the current connection state.
|
||||
* Updates the canonical app-level connection state.
|
||||
*
|
||||
* **This should only be called by [MeshConnectionManager].** Direct mutation from other components would bypass the
|
||||
* transport-to-app reconciliation logic and create state inconsistencies.
|
||||
*
|
||||
* @param connectionState The new [ConnectionState].
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class AndroidRadioControllerImpl(
|
|||
private val nodeRepository: NodeRepository,
|
||||
) : RadioController {
|
||||
|
||||
/** Delegates to [ServiceRepository.connectionState] — the canonical app-level source of truth. */
|
||||
override val connectionState: StateFlow<ConnectionState>
|
||||
get() = serviceRepository.connectionState
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class DirectRadioControllerImpl(
|
|||
private val myNodeNum: Int
|
||||
get() = nodeManager.myNodeNum.value ?: 0
|
||||
|
||||
/** Delegates to [ServiceRepository.connectionState] — the canonical app-level source of truth. */
|
||||
override val connectionState: StateFlow<ConnectionState>
|
||||
get() = serviceRepository.connectionState
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import org.meshtastic.proto.MeshPacket
|
|||
@Suppress("TooManyFunctions")
|
||||
open class ServiceRepositoryImpl : ServiceRepository {
|
||||
|
||||
// Connection state to our radio device
|
||||
// Canonical app-level connection state — written exclusively by MeshConnectionManager.
|
||||
private val _connectionState: MutableStateFlow<ConnectionState> = MutableStateFlow(ConnectionState.Disconnected)
|
||||
override val connectionState: StateFlow<ConnectionState>
|
||||
get() = _connectionState
|
||||
|
|
|
|||
|
|
@ -77,6 +77,15 @@ class SharedRadioInterfaceService(
|
|||
override val supportedDeviceTypes: List<DeviceType>
|
||||
get() = transportFactory.supportedDeviceTypes
|
||||
|
||||
/**
|
||||
* Transport-level connection state reflecting the raw hardware link status.
|
||||
*
|
||||
* Updated directly by [onConnect] and [onDisconnect] when the physical transport (BLE, TCP, Serial) connects or
|
||||
* disconnects. This is consumed exclusively by
|
||||
* [MeshConnectionManager][org.meshtastic.core.repository.MeshConnectionManager], which reconciles it into the
|
||||
* canonical app-level
|
||||
* [ServiceRepository.connectionState][org.meshtastic.core.repository.ServiceRepository.connectionState].
|
||||
*/
|
||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
override val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class FakeRadioController :
|
|||
BaseFake(),
|
||||
RadioController {
|
||||
|
||||
/** Canonical app-level connection state, mirroring [ServiceRepository][connectionState] semantics. */
|
||||
private val _connectionState = mutableStateFlow<ConnectionState>(ConnectionState.Connected)
|
||||
override val connectionState: StateFlow<ConnectionState> = _connectionState
|
||||
|
||||
|
|
|
|||
|
|
@ -28,12 +28,20 @@ import org.meshtastic.core.model.InterfaceId
|
|||
import org.meshtastic.core.model.MeshActivity
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
|
||||
/** A test double for [RadioInterfaceService] that provides an in-memory implementation. */
|
||||
/**
|
||||
* A test double for [RadioInterfaceService] that provides an in-memory implementation.
|
||||
*
|
||||
* The [connectionState] here mirrors the transport-level semantics of the real implementation. In production, only
|
||||
* [MeshConnectionManager][org.meshtastic.core.repository.MeshConnectionManager] observes this flow; tests should verify
|
||||
* that bridging behavior rather than consuming it directly from UI/feature test code (use
|
||||
* [FakeServiceRepository.connectionState] instead).
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class FakeRadioInterfaceService(override val serviceScope: CoroutineScope = MainScope()) : RadioInterfaceService {
|
||||
|
||||
override val supportedDeviceTypes: List<DeviceType> = emptyList()
|
||||
|
||||
/** Transport-level connection state (raw hardware link status). */
|
||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
override val connectionState: StateFlow<ConnectionState> = _connectionState
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import org.meshtastic.proto.MeshPacket
|
|||
|
||||
@Suppress("TooManyFunctions")
|
||||
class FakeServiceRepository : ServiceRepository {
|
||||
/** Canonical app-level connection state — the single source of truth for UI/feature tests. */
|
||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
override val connectionState: StateFlow<ConnectionState> = _connectionState
|
||||
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ class UIViewModel(
|
|||
_sharedContactRequested.value = null
|
||||
}
|
||||
|
||||
// Connection state to our radio device
|
||||
/** Canonical app-level connection state, sourced from [ServiceRepository.connectionState]. */
|
||||
val connectionState
|
||||
get() = serviceRepository.connectionState
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue