mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-10-03 00:54:34 -04:00
refactor(model): parse the selected device address once (#7328)
This commit is contained in:
1 parent
75c191b21c
commit
6708cc5148
15 files changed
+247
-28
No files matched your search
@@ -60,6 +60,7 @@ import org.meshtastic.app.node.component.InlineMap
|
||||
import org.meshtastic.app.node.metrics.getTracerouteMapOverlayInsets
|
||||
import org.meshtastic.app.ui.MainScreen
|
||||
import org.meshtastic.core.barcode.rememberBarcodeScanner
|
||||
import org.meshtastic.core.model.DeviceAddress
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.network.repository.UsbRepository
|
||||
import org.meshtastic.core.nfc.NfcEmulatorEffect
|
||||
@@ -101,7 +102,6 @@ import org.meshtastic.core.ui.util.brandHighlightOrNull
|
||||
import org.meshtastic.core.ui.util.brandPalette
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.core.ui.viewmodel.UIViewModel
|
||||
import org.meshtastic.feature.connections.NO_DEVICE_SELECTED
|
||||
import org.meshtastic.feature.intro.AppIntroductionScreen
|
||||
import org.meshtastic.feature.intro.IntroViewModel
|
||||
import org.meshtastic.feature.map.MapScreen
|
||||
@@ -408,8 +408,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun showConnectionsPageIfNoDeviceSelected() {
|
||||
val selectedAddress = model.currentDeviceAddressFlow.value
|
||||
if (!selectedAddress.isNullOrBlank() && selectedAddress != NO_DEVICE_SELECTED) return
|
||||
if (DeviceAddress.parse(model.currentDeviceAddressFlow.value) != null) return
|
||||
|
||||
handleMeshtasticUri("$DEEP_LINK_BASE_URI/connections".toUri())
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.BuildConfig
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DeviceAddress
|
||||
import org.meshtastic.core.model.service.LockdownState
|
||||
import org.meshtastic.core.navigation.NodesRoute
|
||||
import org.meshtastic.core.navigation.TopLevelDestination
|
||||
@@ -122,10 +123,7 @@ fun MainScreen() {
|
||||
}
|
||||
|
||||
private fun initialRoute(deviceAddress: String?): NavKey =
|
||||
if (deviceAddress.isNullOrSelectedNone()) TopLevelDestination.Connect.route else NodesRoute.Nodes
|
||||
|
||||
/** True when no device address is persisted, or the address is the "none" sentinel (`"n"`). */
|
||||
private fun String?.isNullOrSelectedNone(): Boolean = isNullOrBlank() || this == "n"
|
||||
if (DeviceAddress.parse(deviceAddress) == null) TopLevelDestination.Connect.route else NodesRoute.Nodes
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.meshtastic.core.common.util.isValidDeviceAddress
|
||||
|
||||
/**
|
||||
* A selected-radio address, parsed from its stored form: one [InterfaceId] character followed by the transport's own
|
||||
* identity for the device.
|
||||
*
|
||||
* [raw] is the persisted string and stays the key for the per-device database and every per-device preference, so it is
|
||||
* never rewritten. The legacy `!` BLE prefix therefore keeps its `!` in [raw] and only reads as
|
||||
* [InterfaceId.BLUETOOTH].
|
||||
*/
|
||||
class DeviceAddress private constructor(val raw: String, val interfaceId: InterfaceId) {
|
||||
|
||||
/** The address without its transport prefix: a BLE MAC, a `host:port`, a USB serial key. */
|
||||
val identity: String
|
||||
get() = raw.substring(1)
|
||||
|
||||
override fun equals(other: Any?): Boolean = other is DeviceAddress && other.raw == raw
|
||||
|
||||
override fun hashCode(): Int = raw.hashCode()
|
||||
|
||||
override fun toString(): String = "DeviceAddress($interfaceId)"
|
||||
|
||||
companion object {
|
||||
private const val LEGACY_BLUETOOTH_PREFIX = '!'
|
||||
|
||||
/**
|
||||
* Parses [raw], or returns `null` for no selection: `null`, blank, any no-device sentinel, or an unknown
|
||||
* prefix.
|
||||
*/
|
||||
fun parse(raw: String?): DeviceAddress? {
|
||||
if (raw == null || !isValidDeviceAddress(raw)) return null
|
||||
val prefix = raw.first()
|
||||
val interfaceId =
|
||||
if (prefix == LEGACY_BLUETOOTH_PREFIX) InterfaceId.BLUETOOTH else InterfaceId.forIdChar(prefix)
|
||||
return interfaceId?.takeUnless { it == InterfaceId.NOP }?.let { DeviceAddress(raw, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class DeviceAddressTest {
|
||||
|
||||
@Test
|
||||
fun `every no-device spelling parses to no selection`() {
|
||||
listOf(null, "", " ", "n", "N", "null", ".n", "default", "DEFAULT").forEach { raw ->
|
||||
assertNull(DeviceAddress.parse(raw), "expected no selection for '$raw'")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unknown prefix parses to no selection`() {
|
||||
assertNull(DeviceAddress.parse("z12:34"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `each prefix names its transport and keeps the rest as identity`() {
|
||||
val cases =
|
||||
mapOf(
|
||||
"x11:22:33:44:55:66" to InterfaceId.BLUETOOTH,
|
||||
"s1027:29987:0" to InterfaceId.SERIAL,
|
||||
"t192.168.1.20:4403" to InterfaceId.TCP,
|
||||
"m" to InterfaceId.MOCK,
|
||||
"r" to InterfaceId.REPLAY,
|
||||
)
|
||||
cases.forEach { (raw, interfaceId) ->
|
||||
val address = checkNotNull(DeviceAddress.parse(raw))
|
||||
assertEquals(interfaceId, address.interfaceId)
|
||||
assertEquals(raw, address.raw)
|
||||
assertEquals(raw.drop(1), address.identity)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the legacy bang prefix reads as BLE and keeps its stored form`() {
|
||||
val address = checkNotNull(DeviceAddress.parse("!11:22:33:44:55:66"))
|
||||
|
||||
assertEquals(InterfaceId.BLUETOOTH, address.interfaceId)
|
||||
assertEquals("!11:22:33:44:55:66", address.raw)
|
||||
assertEquals("11:22:33:44:55:66", address.identity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `addresses are equal only when their stored form is`() {
|
||||
assertEquals(DeviceAddress.parse("xAA"), DeviceAddress.parse("xAA"))
|
||||
assertNotEquals(DeviceAddress.parse("xAA"), DeviceAddress.parse("!AA"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toString does not reveal the identity`() {
|
||||
assertEquals("DeviceAddress(BLUETOOTH)", DeviceAddress.parse("x11:22:33:44:55:66").toString())
|
||||
}
|
||||
}
|
||||
+9
-5
@@ -17,7 +17,9 @@
|
||||
package org.meshtastic.core.repository
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.model.DeviceAddress
|
||||
import org.meshtastic.core.model.DeviceType
|
||||
import org.meshtastic.core.model.InterfaceId
|
||||
|
||||
/** Reactive interface for analytics-related preferences. */
|
||||
interface AnalyticsPrefs {
|
||||
@@ -342,15 +344,17 @@ interface RadioPrefs {
|
||||
fun setDevName(name: String?)
|
||||
}
|
||||
|
||||
fun RadioPrefs.isBle() = devAddr.value?.startsWith("x") == true
|
||||
/** The saved selection, parsed; `null` when nothing is selected. */
|
||||
val RadioPrefs.selectedDevice: DeviceAddress?
|
||||
get() = DeviceAddress.parse(devAddr.value)
|
||||
|
||||
fun RadioPrefs.isSerial() = devAddr.value?.startsWith("s") == true
|
||||
fun RadioPrefs.isBle() = selectedDevice?.interfaceId == InterfaceId.BLUETOOTH
|
||||
|
||||
fun RadioPrefs.isMock() = devAddr.value?.startsWith("m") == true
|
||||
fun RadioPrefs.isSerial() = selectedDevice?.interfaceId == InterfaceId.SERIAL
|
||||
|
||||
fun RadioPrefs.isTcp() = devAddr.value?.startsWith("t") == true
|
||||
fun RadioPrefs.isMock() = selectedDevice?.interfaceId == InterfaceId.MOCK
|
||||
|
||||
fun RadioPrefs.isNoop() = devAddr.value?.startsWith("n") == true
|
||||
fun RadioPrefs.isTcp() = selectedDevice?.interfaceId == InterfaceId.TCP
|
||||
|
||||
/** Reactive interface for mesh connection settings. */
|
||||
interface MeshPrefs {
|
||||
|
||||
+14
@@ -30,6 +30,20 @@ class AppPreferencesTest {
|
||||
assertTrue(prefs.isBle())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RadioPrefs isBle returns true for the legacy bang prefix`() {
|
||||
val prefs = FakeRadioPrefs()
|
||||
prefs.setDevAddr("!12345678")
|
||||
assertTrue(prefs.isBle())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RadioPrefs reports no transport for the none sentinel`() {
|
||||
val prefs = FakeRadioPrefs()
|
||||
prefs.setDevAddr("n")
|
||||
assertFalse(prefs.isBle() || prefs.isSerial() || prefs.isTcp() || prefs.isMock())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RadioPrefs isBle returns false for other prefix`() {
|
||||
val prefs = FakeRadioPrefs()
|
||||
|
||||
+3
-4
@@ -17,9 +17,8 @@
|
||||
package org.meshtastic.core.service
|
||||
|
||||
import org.meshtastic.core.common.util.isValidDeviceAddress
|
||||
|
||||
/** Prefix the app uses for a persisted BLE device address. TCP is `t`, USB is `s`. */
|
||||
private const val BLE_ADDRESS_PREFIX = 'x'
|
||||
import org.meshtastic.core.model.DeviceAddress
|
||||
import org.meshtastic.core.model.InterfaceId
|
||||
|
||||
/** What a boot / package-replaced broadcast should do about the previously selected device. */
|
||||
enum class BootReconnectDecision {
|
||||
@@ -53,7 +52,7 @@ enum class BootReconnectDecision {
|
||||
fun bootReconnectDecision(address: String?, hasBluetoothPermission: Boolean): BootReconnectDecision = when {
|
||||
!isValidDeviceAddress(address) -> BootReconnectDecision.NO_DEVICE
|
||||
|
||||
address?.firstOrNull() == BLE_ADDRESS_PREFIX && !hasBluetoothPermission ->
|
||||
DeviceAddress.parse(address)?.interfaceId == InterfaceId.BLUETOOTH && !hasBluetoothPermission ->
|
||||
BootReconnectDecision.BLE_PERMISSION_MISSING
|
||||
|
||||
else -> BootReconnectDecision.START_SERVICE
|
||||
|
||||
+2
-1
@@ -34,6 +34,7 @@ import org.meshtastic.core.common.util.isValidDeviceAddress
|
||||
import org.meshtastic.core.common.util.safeCatching
|
||||
import org.meshtastic.core.common.util.safeCatchingAll
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DeviceAddress
|
||||
import org.meshtastic.core.model.InterfaceId
|
||||
import org.meshtastic.core.repository.MeshConnectionManager
|
||||
import org.meshtastic.core.repository.MeshMessageProcessor
|
||||
@@ -112,7 +113,7 @@ class MeshServiceOrchestrator(
|
||||
// later successful manual connect with no recovery layer for the life of the process. The OS enforces the
|
||||
// permission at the socket, so a genuinely local connect fails exactly as it would have; the difference is
|
||||
// the user now has an explanation and the fix in hand.
|
||||
if (address?.firstOrNull() == InterfaceId.TCP.id && !localNetworkAccess.isGranted()) {
|
||||
if (DeviceAddress.parse(address)?.interfaceId == InterfaceId.TCP && !localNetworkAccess.isGranted()) {
|
||||
Logger.w { "Local network access not granted; the persisted TCP reconnect may time out" }
|
||||
serviceStateWriter.setErrorMessage(
|
||||
// Same shape as ScannerViewModel's scan-failure messages: resource lookup can fail outside a
|
||||
|
||||
+7
-2
@@ -66,6 +66,7 @@ import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.safeCatching
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DeviceAddress
|
||||
import org.meshtastic.core.model.DeviceType
|
||||
import org.meshtastic.core.model.InterfaceId
|
||||
import org.meshtastic.core.model.MeshActivity
|
||||
@@ -113,7 +114,11 @@ private data class UsbRecoveryTriggerState(
|
||||
)
|
||||
|
||||
private fun selectedSerialPresence(address: String?, keys: Set<String>): SelectedSerialPresence {
|
||||
val key = address?.takeIf { it.firstOrNull() == InterfaceId.SERIAL.id }?.drop(1)?.takeIf { it.isNotEmpty() }
|
||||
val key =
|
||||
DeviceAddress.parse(address)
|
||||
?.takeIf { it.interfaceId == InterfaceId.SERIAL }
|
||||
?.identity
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
return SelectedSerialPresence(key = key, present = key != null && key in keys)
|
||||
}
|
||||
|
||||
@@ -929,7 +934,7 @@ class SharedRadioInterfaceService(
|
||||
}
|
||||
throw publicationFailure
|
||||
}
|
||||
runningTransportId = address.firstOrNull()?.let { InterfaceId.forIdChar(it) }
|
||||
runningTransportId = DeviceAddress.parse(address)?.interfaceId
|
||||
isStarted = true
|
||||
startHeartbeat()
|
||||
}
|
||||
|
||||
+8
@@ -53,6 +53,14 @@ class BootReconnectDecisionTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a legacy bang BLE device is not reconnected without the Bluetooth permission`() {
|
||||
assertEquals(
|
||||
BootReconnectDecision.BLE_PERMISSION_MISSING,
|
||||
bootReconnectDecision("!AA:BB:CC:DD:EE:FF", hasBluetoothPermission = false),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TCP and USB devices reconnect regardless of the Bluetooth permission`() {
|
||||
// Gating these would break reconnection for users who have deliberately never granted Bluetooth access.
|
||||
|
||||
+22
@@ -647,6 +647,28 @@ class SharedRadioInterfaceServiceLivenessTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BLE liveness timeout restarts a transport saved with the legacy bang prefix`() = runTest(testDispatcher) {
|
||||
clock = 0L
|
||||
val service = createConnectedService("!AA:BB:CC:DD:EE:FF")
|
||||
try {
|
||||
clock = 65_000L
|
||||
service.checkLiveness()
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
advanceTimeBy(1_000L)
|
||||
|
||||
assertEquals(
|
||||
2,
|
||||
createdTransports.size,
|
||||
"A silent legacy BLE link should be restarted like any BLE link",
|
||||
)
|
||||
assertTrue(createdTransports.first().closeCalled, "Old transport must be closed")
|
||||
} finally {
|
||||
service.disconnect()
|
||||
advanceTimeBy(1_000L)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BLE liveness restart contains factory failure and a later connect can retry`() = runTest(testDispatcher) {
|
||||
clock = 0L
|
||||
|
||||
+15
-6
@@ -35,10 +35,12 @@ import org.koin.core.annotation.KoinViewModel
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DeviceAddress
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.FirmwareUpdateNotice
|
||||
import org.meshtastic.core.model.FirmwareUpdateNoticePolicy
|
||||
import org.meshtastic.core.model.FirmwareUpdateTransport
|
||||
import org.meshtastic.core.model.InterfaceId
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
@@ -245,7 +247,8 @@ class ConnectionsViewModel(
|
||||
combine(firmwareUpdateInputs, localHardware) { inputs, hardware ->
|
||||
val state = inputs.connectionState
|
||||
if (state !is ConnectionState.Connected) return@combine null
|
||||
val transport = inputs.address?.firstOrNull()?.toFirmwareUpdateTransport() ?: return@combine null
|
||||
val transport =
|
||||
DeviceAddress.parse(inputs.address)?.interfaceId?.toFirmwareUpdateTransport() ?: return@combine null
|
||||
val stableRelease = inputs.stableRelease ?: return@combine null
|
||||
val deviceHardware = hardware ?: return@combine null
|
||||
FirmwareUpdateCandidate(
|
||||
@@ -353,9 +356,15 @@ private data class FirmwareUpdateCandidate(
|
||||
val transport: FirmwareUpdateTransport,
|
||||
)
|
||||
|
||||
private fun Char.toFirmwareUpdateTransport(): FirmwareUpdateTransport? = when (this) {
|
||||
'x' -> FirmwareUpdateTransport.Bluetooth
|
||||
's' -> FirmwareUpdateTransport.Serial
|
||||
't' -> FirmwareUpdateTransport.Tcp
|
||||
else -> null
|
||||
private fun InterfaceId.toFirmwareUpdateTransport(): FirmwareUpdateTransport? = when (this) {
|
||||
InterfaceId.BLUETOOTH -> FirmwareUpdateTransport.Bluetooth
|
||||
|
||||
InterfaceId.SERIAL -> FirmwareUpdateTransport.Serial
|
||||
|
||||
InterfaceId.TCP -> FirmwareUpdateTransport.Tcp
|
||||
|
||||
InterfaceId.MOCK,
|
||||
InterfaceId.NOP,
|
||||
InterfaceId.REPLAY,
|
||||
-> null
|
||||
}
|
||||
+26
@@ -370,6 +370,32 @@ class ConnectionsViewModelTest {
|
||||
assertEquals("meshtastic:///firmware/update", dispatchedNotifications.single().deepLinkUri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a device saved with the legacy bang prefix gets the firmware update notice`() = runTest {
|
||||
val target = "tbeam"
|
||||
deviceHardwareRepository.setHardware(
|
||||
hwModel = HardwareModel.TBEAM.value,
|
||||
target = target,
|
||||
device = DeviceHardware(architecture = "esp32", platformioTarget = target),
|
||||
)
|
||||
nodeRepository.setMyId("!local")
|
||||
nodeRepository.setMyNodeInfo(TestDataFactory.createMyNodeInfo(firmwareVersion = "2.7.0", pioEnv = target))
|
||||
nodeRepository.setOurNode(
|
||||
org.meshtastic.core.model.Node(
|
||||
num = 1,
|
||||
user = User.Builder().also { wb -> wb.hw_model = HardwareModel.TBEAM }.build(),
|
||||
),
|
||||
)
|
||||
radioPrefs.setDevAddr("!AA:BB:CC:DD:EE:FF")
|
||||
firmwareReleaseRepository.setManifestTargets("v2.8.0", setOf(target))
|
||||
firmwareReleaseRepository.setStableRelease(FirmwareRelease(id = "v2.8.0"))
|
||||
serviceRepository.setConnectionState(ConnectionState.Connected)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
assertNotNull(viewModel.firmwareUpdateNotice.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does not persist firmware notification dedupe when scheduling is unavailable`() = runTest {
|
||||
val hardwareModel = HardwareModel.TBEAM.value
|
||||
|
||||
@@ -80,6 +80,7 @@ import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.common.log.InMemoryLogBuffer
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.database.desktopDataDir
|
||||
import org.meshtastic.core.model.DeviceAddress
|
||||
import org.meshtastic.core.navigation.MultiBackstack
|
||||
import org.meshtastic.core.navigation.SettingsRoute
|
||||
import org.meshtastic.core.navigation.TopLevelDestination
|
||||
@@ -396,7 +397,7 @@ private fun ApplicationScope.MeshtasticWindow(
|
||||
val multiBackstack =
|
||||
rememberMultiBackstack(
|
||||
// Land on Connections for first-run / no-device-selected; otherwise on Nodes.
|
||||
if (uiViewModel.currentDeviceAddressFlow.value.let { it.isNullOrBlank() || it == "n" }) {
|
||||
if (DeviceAddress.parse(uiViewModel.currentDeviceAddressFlow.value) == null) {
|
||||
TopLevelDestination.Connect.route
|
||||
} else {
|
||||
TopLevelDestination.Nodes.route
|
||||
|
||||
+4
-2
@@ -52,6 +52,7 @@ import org.meshtastic.core.datastore.BootloaderWarningDataSource
|
||||
import org.meshtastic.core.datastore.FirmwareRecoveryDataSource
|
||||
import org.meshtastic.core.datastore.model.PendingFirmwareRecovery
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DeviceAddress
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.InterfaceId
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
@@ -67,6 +68,7 @@ import org.meshtastic.core.repository.RadioPrefs
|
||||
import org.meshtastic.core.repository.isBle
|
||||
import org.meshtastic.core.repository.isSerial
|
||||
import org.meshtastic.core.repository.isTcp
|
||||
import org.meshtastic.core.repository.selectedDevice
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.UiText
|
||||
import org.meshtastic.core.resources.firmware_maintenance_cdc_unblock_failed
|
||||
@@ -274,7 +276,7 @@ class FirmwareUpdateViewModel(
|
||||
_state.value = FirmwareUpdateState.Checking
|
||||
safeCatching {
|
||||
val ourNode = nodeRepository.myNodeInfo.value
|
||||
val address = radioPrefs.devAddr.value?.drop(1)
|
||||
val address = radioPrefs.selectedDevice?.identity
|
||||
if (address == null || ourNode == null) {
|
||||
// Not connected: offer to re-flash a device stranded in bootloader mode if we saved a
|
||||
// recovery record when its (now-interrupted) update was triggered. Otherwise, no device.
|
||||
@@ -1329,7 +1331,7 @@ private fun isValidBluetoothAddress(address: String?): Boolean =
|
||||
address != null && BLUETOOTH_ADDRESS_REGEX.matches(address)
|
||||
|
||||
private fun isBluetoothInterfaceAddress(address: String): Boolean =
|
||||
address.startsWith(InterfaceId.BLUETOOTH.id) || address.startsWith("!")
|
||||
DeviceAddress.parse(address)?.interfaceId == InterfaceId.BLUETOOTH
|
||||
|
||||
private fun FirmwareReleaseRepository.getReleaseFlow(type: FirmwareReleaseType): Flow<FirmwareRelease?> = when (type) {
|
||||
FirmwareReleaseType.STABLE -> stableRelease
|
||||
|
||||
Reference in new issue
Block a user