refactor(transport): complete transport architecture overhaul — extract callback, wire BleReconnectPolicy, fix safety issues (#5080)

This commit is contained in:
James Rich 2026-04-11 23:22:18 -05:00 committed by GitHub
parent 962c619c4c
commit e85300531e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 1184 additions and 1018 deletions

View file

@ -54,8 +54,8 @@ open class ScannerViewModel(
private val dispatchers: org.meshtastic.core.di.CoroutineDispatchers,
private val bleScanner: org.meshtastic.core.ble.BleScanner? = null,
) : ViewModel() {
private val _showMockInterface = MutableStateFlow(false)
val showMockInterface: StateFlow<Boolean> = _showMockInterface.asStateFlow()
private val _showMockTransport = MutableStateFlow(false)
val showMockTransport: StateFlow<Boolean> = _showMockTransport.asStateFlow()
private val _errorText = MutableStateFlow<String?>(null)
val errorText: StateFlow<String?> = _errorText.asStateFlow()
@ -68,7 +68,7 @@ open class ScannerViewModel(
private var scanJob: kotlinx.coroutines.Job? = null
init {
_showMockInterface.value = radioInterfaceService.isMockInterface()
_showMockTransport.value = radioInterfaceService.isMockTransport()
}
fun startBleScan() {
@ -77,25 +77,26 @@ open class ScannerViewModel(
isBleScanningState.value = true
scannedBleDevices.value = emptyMap()
scanJob = viewModelScope.launch {
try {
bleScanner
.scan(
timeout = kotlin.time.Duration.INFINITE,
serviceUuid = org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID,
)
.flowOn(dispatchers.io)
.collect { device ->
if (!scannedBleDevices.value.containsKey(device.address)) {
scannedBleDevices.update { current -> current + (device.address to device) }
scanJob =
viewModelScope.launch {
try {
bleScanner
.scan(
timeout = kotlin.time.Duration.INFINITE,
serviceUuid = org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID,
)
.flowOn(dispatchers.io)
.collect { device ->
if (!scannedBleDevices.value.containsKey(device.address)) {
scannedBleDevices.update { current -> current + (device.address to device) }
}
}
}
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
co.touchlab.kermit.Logger.w(e) { "BLE scan failed" }
} finally {
isBleScanningState.value = false
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
co.touchlab.kermit.Logger.w(e) { "BLE scan failed" }
} finally {
isBleScanningState.value = false
}
}
}
}
fun stopBleScan() {
@ -105,7 +106,7 @@ open class ScannerViewModel(
}
private val discoveredDevicesFlow =
showMockInterface
showMockTransport
.flatMapLatest { showMock -> getDiscoveredDevicesUseCase.invoke(showMock) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)

View file

@ -167,17 +167,19 @@ fun ConnectionsScreen(
Spacer(modifier = Modifier.height(4.dp))
val uiState =
when {
connectionState is ConnectionState.Connected && ourNode != null -> 2
connectionState is ConnectionState.Connected && ourNode != null ->
ConnectionUiState.CONNECTED_WITH_NODE
connectionState is ConnectionState.Connected ||
connectionState == ConnectionState.Connecting ||
selectedDevice != NO_DEVICE_SELECTED -> 1
selectedDevice != NO_DEVICE_SELECTED -> ConnectionUiState.CONNECTING
else -> 0
else -> ConnectionUiState.NO_DEVICE
}
Crossfade(targetState = uiState, label = "connection_state") { state ->
when (state) {
2 ->
ConnectionUiState.CONNECTED_WITH_NODE ->
ConnectedDeviceContent(
ourNode = ourNode,
regionUnset = regionUnset,
@ -191,7 +193,7 @@ fun ConnectionsScreen(
},
)
1 ->
ConnectionUiState.CONNECTING ->
ConnectingDeviceContent(
connectionState = connectionState,
selectedDevice = selectedDevice,
@ -208,7 +210,9 @@ fun ConnectionsScreen(
}
var selectedDeviceType by remember { mutableStateOf(DeviceType.BLE) }
LaunchedEffect(Unit) { DeviceType.fromAddress(selectedDevice)?.let { selectedDeviceType = it } }
LaunchedEffect(selectedDevice) {
DeviceType.fromAddress(selectedDevice)?.let { selectedDeviceType = it }
}
val supportedDeviceTypes = scanModel.supportedDeviceTypes
@ -369,3 +373,15 @@ private fun NoDeviceContent() {
)
}
}
/** Visual state for the connection screen's [Crossfade] animation. */
private enum class ConnectionUiState {
/** No device is selected. */
NO_DEVICE,
/** A device is selected or we are actively connecting. */
CONNECTING,
/** Connected with node info available. */
CONNECTED_WITH_NODE,
}

View file

@ -42,6 +42,10 @@ import org.meshtastic.core.resources.connecting
import org.meshtastic.core.resources.disconnect
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
/**
* Displays the currently connecting (or connected) device with its name, address, connection status, and a disconnect
* button.
*/
@Composable
fun ConnectingDeviceInfo(
connectionState: ConnectionState,

View file

@ -53,7 +53,7 @@ class ScannerViewModelTest {
@BeforeTest
fun setUp() {
every { radioInterfaceService.isMockInterface() } returns false
every { radioInterfaceService.isMockTransport() } returns false
every { radioInterfaceService.currentDeviceAddressFlow } returns MutableStateFlow(null)
every { radioInterfaceService.supportedDeviceTypes } returns emptyList()