mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-23 04:05:10 -04:00
feat(settings): gate config fields on the schema's firmware versions (#7262)
This commit is contained in:
1 parent
da71c2810d
commit
d635ba4603
9 files changed
+179
-88
No files matched your search
@@ -17,6 +17,7 @@
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.meshtastic.core.model.util.isDebug
|
||||
import org.meshtastic.proto.FieldMetadata
|
||||
|
||||
/**
|
||||
* Defines the capabilities and feature support based on the device firmware version.
|
||||
@@ -31,6 +32,22 @@ data class Capabilities(val firmwareVersion: String?, internal val forceEnableAl
|
||||
|
||||
private fun atLeast(min: DeviceVersion): Boolean = forceEnableAll || (version != null && version >= min)
|
||||
|
||||
/**
|
||||
* Whether a config field is worth offering on this firmware, from the version gates its schema declares. Below
|
||||
* `since_firmware` the node ignores the field. At or above `deprecated_since` it is shown only while [isSet], so a
|
||||
* value the node still holds stays visible instead of being silently kept.
|
||||
*/
|
||||
fun offers(field: FieldMetadata, isSet: Boolean = false): Boolean {
|
||||
val arrived = field.since_firmware?.let(::gate)?.let(::atLeast) ?: true
|
||||
val retired =
|
||||
field.deprecated_since?.let(::gate)?.let { !forceEnableAll && version != null && version >= it } ?: false
|
||||
return arrived && (!retired || isSet)
|
||||
}
|
||||
|
||||
// The schema declares these; an unparseable one must fail here rather than silently pass every gate.
|
||||
private fun gate(declared: String): DeviceVersion =
|
||||
DeviceVersion(declared).also { require(it.isValid) { "Unparseable firmware version in schema: $declared" } }
|
||||
|
||||
/** Ability to mute notifications from specific nodes via admin messages. */
|
||||
val canMuteNode = atLeast(V2_7_18)
|
||||
|
||||
@@ -40,17 +57,14 @@ data class Capabilities(val firmwareVersion: String?, internal val forceEnableAl
|
||||
/** Ability to send verified shared contacts. Supported since firmware v2.7.12. */
|
||||
val canSendVerifiedContacts = atLeast(V2_7_12)
|
||||
|
||||
/** Ability to toggle device telemetry globally via module config. Supported since firmware v2.7.12. */
|
||||
val canToggleTelemetryEnabled = atLeast(V2_7_12)
|
||||
|
||||
/** Ability to toggle the 'is_unmessageable' flag in user config. Supported since firmware v2.6.9. */
|
||||
val canToggleUnmessageable = atLeast(V2_6_9)
|
||||
|
||||
/** Support for sharing contact information via QR codes. Supported since firmware v2.6.8. */
|
||||
val supportsQrCodeSharing = atLeast(V2_6_8)
|
||||
|
||||
/** Support for Status Message module. Supported since firmware v2.8.0. */
|
||||
val supportsStatusMessage = atLeast(V2_8_0)
|
||||
/** Support for Status Message module. Supported since firmware v2.7.20. */
|
||||
val supportsStatusMessage = atLeast(V2_7_20)
|
||||
|
||||
/**
|
||||
* Support for TAK (ATAK) module configuration. Gated to firmware v2.8.0.
|
||||
@@ -125,6 +139,7 @@ data class Capabilities(val firmwareVersion: String?, internal val forceEnableAl
|
||||
private val V2_6_10 = DeviceVersion("2.6.10")
|
||||
private val V2_7_12 = DeviceVersion("2.7.12")
|
||||
private val V2_7_18 = DeviceVersion("2.7.18")
|
||||
private val V2_7_20 = DeviceVersion("2.7.20")
|
||||
private val V2_8_0 = DeviceVersion("2.8.0")
|
||||
private val UNRELEASED = DeviceVersion("9.9.9")
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.meshtastic.proto.FieldMetadata
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@@ -50,12 +52,6 @@ class CapabilitiesTest {
|
||||
assertTrue(caps("2.7.12").canSendVerifiedContacts)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canToggleTelemetryEnabled_requires_V2_7_12() {
|
||||
assertFalse(caps("2.7.11").canToggleTelemetryEnabled)
|
||||
assertTrue(caps("2.7.12").canToggleTelemetryEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canToggleUnmessageable_requires_V2_6_9() {
|
||||
assertFalse(caps("2.6.8").canToggleUnmessageable)
|
||||
@@ -75,9 +71,47 @@ class CapabilitiesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun supportsStatusMessage_requires_V2_8_0() {
|
||||
assertFalse(caps("2.7.21").supportsStatusMessage)
|
||||
assertTrue(caps("2.8.0").supportsStatusMessage)
|
||||
fun supportsStatusMessage_requires_V2_7_20() {
|
||||
assertFalse(caps("2.7.19").supportsStatusMessage)
|
||||
assertTrue(caps("2.7.20").supportsStatusMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun offers_hides_a_field_below_since_firmware() {
|
||||
val field = FieldMetadata.Builder().since_firmware("2.7.13").build()
|
||||
assertFalse(caps("2.7.12").offers(field))
|
||||
assertTrue(caps("2.7.13").offers(field))
|
||||
assertFalse(caps(null).offers(field))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun offers_hides_a_deprecated_field_at_deprecated_since_unless_set() {
|
||||
val field = FieldMetadata.Builder().deprecated_since("2.8.0").build()
|
||||
assertTrue(caps("2.7.26").offers(field))
|
||||
assertFalse(caps("2.8.0").offers(field))
|
||||
assertTrue(caps("2.8.0").offers(field, isSet = true))
|
||||
// Unknown firmware cannot be shown to have retired the field.
|
||||
assertTrue(caps(null).offers(field))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun offers_an_unannotated_field_everywhere() {
|
||||
val field = FieldMetadata.Builder().build()
|
||||
assertTrue(caps(null).offers(field))
|
||||
assertTrue(caps("2.3.15").offers(field))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun offers_everything_when_forceEnableAll() {
|
||||
val c = Capabilities(firmwareVersion = null, forceEnableAll = true)
|
||||
assertTrue(c.offers(FieldMetadata.Builder().since_firmware("9.9.9").build()))
|
||||
assertTrue(c.offers(FieldMetadata.Builder().deprecated_since("1.0.0").build()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun offers_rejects_an_unparseable_schema_version() {
|
||||
val field = FieldMetadata.Builder().since_firmware("soon").build()
|
||||
assertFailsWith<IllegalArgumentException> { caps("2.8.0").offers(field) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -164,7 +198,6 @@ class CapabilitiesTest {
|
||||
assertFalse(c.canMuteNode)
|
||||
assertFalse(c.canRequestNeighborInfo)
|
||||
assertFalse(c.canSendVerifiedContacts)
|
||||
assertFalse(c.canToggleTelemetryEnabled)
|
||||
assertFalse(c.canToggleUnmessageable)
|
||||
assertFalse(c.supportsQrCodeSharing)
|
||||
assertFalse(c.supportsSecondaryChannelLocation)
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ class StatusMessageActionGateTest {
|
||||
|
||||
@Test
|
||||
fun `older firmware does not offer the action`() {
|
||||
val old = node(num = 1, firmware = "2.7.21")
|
||||
val old = node(num = 1, firmware = "2.7.19")
|
||||
|
||||
assertFalse(canEditStatusMessage(old, old, ConnectionState.Connected))
|
||||
}
|
||||
|
||||
+26
-13
@@ -23,6 +23,7 @@ import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
@@ -30,6 +31,7 @@ import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.allow_input_source
|
||||
import org.meshtastic.core.resources.canned_message
|
||||
@@ -52,11 +54,14 @@ import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.RebootBehavior
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.allow_input_source
|
||||
|
||||
@Suppress("DEPRECATION", "LongMethod")
|
||||
@Composable
|
||||
fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val firmwareVersion = state.metadata?.firmware_version
|
||||
val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) }
|
||||
val cannedMessageConfig = state.moduleConfig.canned_message ?: ModuleConfig.CannedMessageConfig.Builder().build()
|
||||
val messages = state.cannedMessageMessages
|
||||
val formState = rememberConfigState(initialValue = cannedMessageConfig)
|
||||
@@ -176,19 +181,27 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.allow_input_source),
|
||||
value = formState.value.allow_input_source,
|
||||
maxSize = 63, // allow_input_source max_size:16
|
||||
enabled = state.connected,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.allow_input_source = it }.build()
|
||||
},
|
||||
)
|
||||
if (
|
||||
capabilities.offers(
|
||||
ModuleConfig.CannedMessageConfig.allow_input_source,
|
||||
isSet = formState.value.allow_input_source.isNotEmpty(),
|
||||
)
|
||||
) {
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.allow_input_source),
|
||||
value = formState.value.allow_input_source,
|
||||
maxSize = 63, // allow_input_source max_size:16
|
||||
enabled = state.connected,
|
||||
isError = false,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
formState.value =
|
||||
formState.value.newBuilder().also { wb -> wb.allow_input_source = it }.build()
|
||||
},
|
||||
)
|
||||
}
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.send_bell),
|
||||
checked = formState.value.send_bell,
|
||||
|
||||
+37
-22
@@ -23,6 +23,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.advanced
|
||||
import org.meshtastic.core.resources.always_point_north
|
||||
@@ -55,11 +56,15 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.compass_north_top
|
||||
import org.meshtastic.proto.use_12h_clock
|
||||
|
||||
@Suppress("DEPRECATION", "LongMethod")
|
||||
@Composable
|
||||
fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val firmwareVersion = state.metadata?.firmware_version
|
||||
val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) }
|
||||
val displayConfig = state.radioConfig.display ?: Config.DisplayConfig.Builder().build()
|
||||
val formState = rememberConfigState(initialValue = displayConfig)
|
||||
|
||||
@@ -77,28 +82,38 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
) {
|
||||
item {
|
||||
TitledCard(title = stringResource(Res.string.display_config)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.always_point_north),
|
||||
summary = stringResource(Res.string.config_display_compass_north_top_summary),
|
||||
checked = formState.value.compass_north_top,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.compass_north_top = it }.build()
|
||||
},
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.use_12h_format),
|
||||
summary = stringResource(Res.string.display_time_in_12h_format),
|
||||
enabled = state.connected,
|
||||
checked = formState.value.use_12h_clock,
|
||||
onCheckedChange = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.use_12h_clock = it }.build()
|
||||
},
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
if (
|
||||
capabilities.offers(
|
||||
Config.DisplayConfig.compass_north_top,
|
||||
isSet = formState.value.compass_north_top,
|
||||
)
|
||||
) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.always_point_north),
|
||||
summary = stringResource(Res.string.config_display_compass_north_top_summary),
|
||||
checked = formState.value.compass_north_top,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = {
|
||||
formState.value =
|
||||
formState.value.newBuilder().also { wb -> wb.compass_north_top = it }.build()
|
||||
},
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (capabilities.offers(Config.DisplayConfig.use_12h_clock)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.use_12h_format),
|
||||
summary = stringResource(Res.string.display_time_in_12h_format),
|
||||
enabled = state.connected,
|
||||
checked = formState.value.use_12h_clock,
|
||||
onCheckedChange = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.use_12h_clock = it }.build()
|
||||
},
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.bold_heading),
|
||||
summary = stringResource(Res.string.config_display_heading_bold_summary),
|
||||
|
||||
+19
-12
@@ -36,6 +36,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -48,6 +49,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.model.MqttConnectionState
|
||||
import org.meshtastic.core.model.MqttProbeStatus
|
||||
import org.meshtastic.core.network.repository.effectiveTlsEnabled
|
||||
@@ -91,13 +93,16 @@ import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.RebootBehavior
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.json_enabled
|
||||
|
||||
// json_enabled is deprecated in the protobuf schema but remains the only toggle for MQTT JSON
|
||||
// publish/consume, so the settings UI must keep exposing it until the proto provides a replacement.
|
||||
// json_enabled still drives MQTT JSON on firmware below its deprecated_since; beyond that the schema gate hides it
|
||||
// unless the node still holds it set, so a stale value stays visible.
|
||||
@Suppress("DEPRECATION")
|
||||
@Composable
|
||||
fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val firmwareVersion = state.metadata?.firmware_version
|
||||
val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) }
|
||||
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
val mqttProxyState by viewModel.mqttConnectionState.collectAsStateWithLifecycle()
|
||||
val mqttProxyActive by viewModel.mqttProxyActive.collectAsStateWithLifecycle()
|
||||
@@ -217,16 +222,18 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.json_output_enabled),
|
||||
checked = formState.value.json_enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.json_enabled = it }.build()
|
||||
},
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
if (capabilities.offers(ModuleConfig.MQTTConfig.json_enabled, isSet = formState.value.json_enabled)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.json_output_enabled),
|
||||
checked = formState.value.json_enabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.json_enabled = it }.build()
|
||||
},
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
MqttTlsPreference(
|
||||
enabled = state.connected,
|
||||
address = formState.value.address,
|
||||
|
||||
+28
-21
@@ -28,6 +28,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -39,6 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.common.util.extractWifiCredentials
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.model.util.handleMeshtasticUri
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.advanced
|
||||
@@ -83,6 +85,7 @@ import org.meshtastic.core.ui.util.LocalNfcScannerSupported
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.RebootBehavior
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.enabled_protocols
|
||||
|
||||
@Composable
|
||||
private fun ScanErrorDialog(onDismiss: () -> Unit = {}) =
|
||||
@@ -98,6 +101,8 @@ private fun formatIpAddress(ipAddress: Int): String = "${(ipAddress) and 0xFF}."
|
||||
@Composable
|
||||
fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onOpenNfcSettings: () -> Unit = {}) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val firmwareVersion = state.metadata?.firmware_version
|
||||
val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) }
|
||||
val networkConfig = state.radioConfig.network ?: Config.NetworkConfig.Builder().build()
|
||||
val formState = rememberConfigState(initialValue = networkConfig)
|
||||
|
||||
@@ -296,27 +301,29 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.udp_enabled),
|
||||
summary = stringResource(Res.string.config_network_udp_enabled_summary),
|
||||
checked =
|
||||
formState.value.enabled_protocols and Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value !=
|
||||
0,
|
||||
onCheckedChange = { enabled ->
|
||||
val flags =
|
||||
if (enabled) {
|
||||
formState.value.enabled_protocols or
|
||||
Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value
|
||||
} else {
|
||||
formState.value.enabled_protocols and
|
||||
Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value.inv()
|
||||
}
|
||||
formState.value =
|
||||
formState.value.newBuilder().also { wb -> wb.enabled_protocols = flags }.build()
|
||||
},
|
||||
enabled = state.connected,
|
||||
)
|
||||
HorizontalDivider()
|
||||
if (capabilities.offers(Config.NetworkConfig.enabled_protocols)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.udp_enabled),
|
||||
summary = stringResource(Res.string.config_network_udp_enabled_summary),
|
||||
checked =
|
||||
formState.value.enabled_protocols and
|
||||
Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value != 0,
|
||||
onCheckedChange = { enabled ->
|
||||
val flags =
|
||||
if (enabled) {
|
||||
formState.value.enabled_protocols or
|
||||
Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value
|
||||
} else {
|
||||
formState.value.enabled_protocols and
|
||||
Config.NetworkConfig.ProtocolFlags.UDP_BROADCAST.value.inv()
|
||||
}
|
||||
formState.value =
|
||||
formState.value.newBuilder().also { wb -> wb.enabled_protocols = flags }.build()
|
||||
},
|
||||
enabled = state.connected,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.ipv4_mode),
|
||||
enabled = state.connected,
|
||||
|
||||
+2
-1
@@ -47,6 +47,7 @@ import org.meshtastic.feature.settings.radio.RebootBehavior
|
||||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.device_telemetry_enabled
|
||||
|
||||
@Composable
|
||||
fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
@@ -72,7 +73,7 @@ fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
) {
|
||||
item {
|
||||
TitledCard(title = stringResource(Res.string.telemetry_config)) {
|
||||
if (capabilities.canToggleTelemetryEnabled) {
|
||||
if (capabilities.offers(ModuleConfig.TelemetryConfig.device_telemetry_enabled)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(Res.string.device_telemetry_enabled),
|
||||
summary = stringResource(Res.string.device_telemetry_enabled_summary),
|
||||
|
||||
+3
-3
@@ -149,7 +149,7 @@ class RadioConfigViewModelTest {
|
||||
Node(
|
||||
num = 123,
|
||||
user = User.Builder().also { wb -> wb.id = "!123" }.build(),
|
||||
metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.21" }.build(),
|
||||
metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.19" }.build(),
|
||||
)
|
||||
nodeRepository.setNodes(listOf(node))
|
||||
viewModel = createViewModel(destNum = 123)
|
||||
@@ -196,7 +196,7 @@ class RadioConfigViewModelTest {
|
||||
Node(
|
||||
num = 456,
|
||||
user = User.Builder().also { wb -> wb.id = "!456" }.build(),
|
||||
metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.21" }.build(),
|
||||
metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.19" }.build(),
|
||||
)
|
||||
nodeRepository.setNodes(listOf(localNode, remoteNode))
|
||||
nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 100))
|
||||
@@ -2035,7 +2035,7 @@ class RadioConfigViewModelTest {
|
||||
Node(
|
||||
num = 456,
|
||||
user = User.Builder().also { wb -> wb.id = "!456" }.build(),
|
||||
metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.21" }.build(),
|
||||
metadata = DeviceMetadata.Builder().also { wb -> wb.firmware_version = "2.7.19" }.build(),
|
||||
)
|
||||
val packetFlow = MutableSharedFlow<MeshPacket>()
|
||||
val maxRetransmit = org.meshtastic.core.resources.UiText.DynamicString("Max Retransmission Reached")
|
||||
|
||||
Reference in new issue
Block a user