mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-02-06 22:02:37 -05:00
feat(settings): Add firmware capability checks for settings (#4403)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -56,6 +56,14 @@ data class Capabilities(val firmwareVersion: String?, internal val forceEnableAl
|
||||
val supportsQrCodeSharing: Boolean
|
||||
get() = isSupported("2.6.8")
|
||||
|
||||
/** Support for Status Message module. Supported since firmware v2.7.17. */
|
||||
val supportsStatusMessage: Boolean
|
||||
get() = isSupported("2.7.17")
|
||||
|
||||
/** Support for location sharing on secondary channels. Supported since firmware v2.6.10. */
|
||||
val supportsSecondaryChannelLocation: Boolean
|
||||
get() = isSupported("2.6.10")
|
||||
|
||||
/** Support for ESP32 Unified OTA. Supported since firmware v2.7.18. */
|
||||
val supportsEsp32Ota: Boolean
|
||||
get() = isSupported("2.7.18")
|
||||
|
||||
@@ -65,6 +65,18 @@ class CapabilitiesTest {
|
||||
assertTrue(caps("2.6.8").supportsQrCodeSharing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `supportsSecondaryChannelLocation requires v2 6 10`() {
|
||||
assertFalse(caps("2.6.9").supportsSecondaryChannelLocation)
|
||||
assertTrue(caps("2.6.10").supportsSecondaryChannelLocation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `supportsStatusMessage requires v2 7 17`() {
|
||||
assertFalse(caps("2.7.16").supportsStatusMessage)
|
||||
assertTrue(caps("2.7.17").supportsStatusMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `null firmware returns all false`() {
|
||||
val c = caps(null)
|
||||
@@ -74,6 +86,8 @@ class CapabilitiesTest {
|
||||
assertFalse(c.canToggleTelemetryEnabled)
|
||||
assertFalse(c.canToggleUnmessageable)
|
||||
assertFalse(c.supportsQrCodeSharing)
|
||||
assertFalse(c.supportsSecondaryChannelLocation)
|
||||
assertFalse(c.supportsStatusMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -85,6 +99,8 @@ class CapabilitiesTest {
|
||||
assertFalse(c.canToggleTelemetryEnabled)
|
||||
assertFalse(c.canToggleUnmessageable)
|
||||
assertFalse(c.supportsQrCodeSharing)
|
||||
assertFalse(c.supportsSecondaryChannelLocation)
|
||||
assertFalse(c.supportsStatusMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -96,6 +112,8 @@ class CapabilitiesTest {
|
||||
assertTrue(c.canToggleTelemetryEnabled)
|
||||
assertTrue(c.canToggleUnmessageable)
|
||||
assertTrue(c.supportsQrCodeSharing)
|
||||
assertTrue(c.supportsSecondaryChannelLocation)
|
||||
assertTrue(c.supportsStatusMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -107,5 +125,7 @@ class CapabilitiesTest {
|
||||
assertTrue(c.canToggleTelemetryEnabled)
|
||||
assertTrue(c.canToggleUnmessageable)
|
||||
assertTrue(c.supportsQrCodeSharing)
|
||||
assertTrue(c.supportsSecondaryChannelLocation)
|
||||
assertTrue(c.supportsStatusMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,16 +20,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Forward
|
||||
import androidx.compose.material.icons.automirrored.filled.Message
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.Cloud
|
||||
import androidx.compose.material.icons.filled.DataUsage
|
||||
import androidx.compose.material.icons.filled.LightMode
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.People
|
||||
import androidx.compose.material.icons.filled.PermScanWifi
|
||||
import androidx.compose.material.icons.filled.Sensors
|
||||
import androidx.compose.material.icons.filled.SettingsRemote
|
||||
import androidx.compose.material.icons.filled.Speed
|
||||
import androidx.compose.material.icons.filled.Usb
|
||||
import androidx.compose.material.icons.rounded.Cloud
|
||||
import androidx.compose.material.icons.rounded.DataUsage
|
||||
import androidx.compose.material.icons.rounded.LightMode
|
||||
@@ -42,6 +32,7 @@ import androidx.compose.material.icons.rounded.Speed
|
||||
import androidx.compose.material.icons.rounded.Usb
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.strings.Res
|
||||
@@ -62,7 +53,13 @@ import org.meshtastic.core.strings.telemetry
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.MeshProtos.DeviceMetadata
|
||||
|
||||
enum class ModuleRoute(val title: StringResource, val route: Route, val icon: ImageVector?, val type: Int = 0) {
|
||||
enum class ModuleRoute(
|
||||
val title: StringResource,
|
||||
val route: Route,
|
||||
val icon: ImageVector?,
|
||||
val type: Int = 0,
|
||||
val isSupported: (Capabilities) -> Boolean = { true },
|
||||
) {
|
||||
MQTT(
|
||||
Res.string.mqtt,
|
||||
SettingsRoutes.MQTT,
|
||||
@@ -146,6 +143,7 @@ enum class ModuleRoute(val title: StringResource, val route: Route, val icon: Im
|
||||
SettingsRoutes.StatusMessage,
|
||||
Icons.AutoMirrored.Default.Message,
|
||||
AdminProtos.AdminMessage.ModuleConfigType.STATUSMESSAGE_CONFIG_VALUE,
|
||||
isSupported = { it.supportsStatusMessage },
|
||||
),
|
||||
;
|
||||
|
||||
@@ -153,10 +151,11 @@ enum class ModuleRoute(val title: StringResource, val route: Route, val icon: Im
|
||||
get() = 1 shl ordinal
|
||||
|
||||
companion object {
|
||||
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ModuleRoute> = entries.filter {
|
||||
when (metadata) {
|
||||
null -> true // Include all routes if metadata is null
|
||||
else -> metadata.excludedModules and it.bitfield == 0
|
||||
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ModuleRoute> {
|
||||
val capabilities = Capabilities(metadata?.firmwareVersion)
|
||||
return entries.filter {
|
||||
val isExcluded = metadata != null && (metadata.excludedModules and it.bitfield != 0)
|
||||
!isExcluded && it.isSupported(capabilities)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-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
|
||||
@@ -14,7 +14,6 @@
|
||||
* 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.feature.settings.radio.channel
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -53,8 +52,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add
|
||||
import org.meshtastic.core.strings.cancel
|
||||
@@ -73,7 +72,6 @@ import org.meshtastic.feature.settings.radio.channel.component.ChannelConfigHead
|
||||
import org.meshtastic.feature.settings.radio.channel.component.ChannelLegend
|
||||
import org.meshtastic.feature.settings.radio.channel.component.ChannelLegendDialog
|
||||
import org.meshtastic.feature.settings.radio.channel.component.EditChannelDialog
|
||||
import org.meshtastic.feature.settings.radio.channel.component.SECONDARY_CHANNEL_EPOCH
|
||||
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
|
||||
import org.meshtastic.proto.ChannelProtos.ChannelSettings
|
||||
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig
|
||||
@@ -114,8 +112,7 @@ private fun ChannelConfigScreen(
|
||||
val primarySettings = settingsList.getOrNull(0) ?: return
|
||||
val modemPresetName by remember(loraConfig) { mutableStateOf(Channel(loraConfig = loraConfig).name) }
|
||||
val primaryChannel by remember(loraConfig) { mutableStateOf(Channel(primarySettings, loraConfig)) }
|
||||
val fwVersion by
|
||||
remember(firmwareVersion) { mutableStateOf(DeviceVersion(firmwareVersion.substringBeforeLast("."))) }
|
||||
val capabilities by remember(firmwareVersion) { mutableStateOf(Capabilities(firmwareVersion)) }
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
val settingsListInput =
|
||||
@@ -156,7 +153,7 @@ private fun ChannelConfigScreen(
|
||||
}
|
||||
|
||||
if (showChannelLegendDialog) {
|
||||
ChannelLegendDialog(fwVersion) { showChannelLegendDialog = false }
|
||||
ChannelLegendDialog(capabilities) { showChannelLegendDialog = false }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -211,7 +208,7 @@ private fun ChannelConfigScreen(
|
||||
|
||||
ChannelLegend { showChannelLegendDialog = true }
|
||||
|
||||
val locationChannel = determineLocationSharingChannel(fwVersion, settingsListInput.toList())
|
||||
val locationChannel = determineLocationSharingChannel(capabilities, settingsListInput.toList())
|
||||
|
||||
LazyColumn(
|
||||
modifier =
|
||||
@@ -276,13 +273,13 @@ private fun ChannelConfigScreen(
|
||||
/**
|
||||
* Determines what [Channel] if any is enabled to conduct automatic location sharing.
|
||||
*
|
||||
* @param firmwareVersion of the connected node.
|
||||
* @param capabilities of the connected node.
|
||||
* @param settingsList Current list of channels on the node.
|
||||
* @return the index of the channel within `settingsList`.
|
||||
*/
|
||||
private fun determineLocationSharingChannel(firmwareVersion: DeviceVersion, settingsList: List<ChannelSettings>): Int {
|
||||
private fun determineLocationSharingChannel(capabilities: Capabilities, settingsList: List<ChannelSettings>): Int {
|
||||
var output = -1
|
||||
if (firmwareVersion >= DeviceVersion(asString = SECONDARY_CHANNEL_EPOCH)) {
|
||||
if (capabilities.supportsSecondaryChannelLocation) {
|
||||
/* Essentially the first index with the setting enabled */
|
||||
for ((i, settings) in settingsList.withIndex()) {
|
||||
if (settings.moduleSettings.positionPrecision > 0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-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
|
||||
@@ -14,7 +14,6 @@
|
||||
* 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.feature.settings.radio.channel.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -45,7 +44,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.channel_features
|
||||
import org.meshtastic.core.strings.downlink_enabled
|
||||
@@ -64,12 +63,27 @@ import org.meshtastic.core.strings.security_icon_help_dismiss
|
||||
import org.meshtastic.core.strings.uplink_enabled
|
||||
import org.meshtastic.core.strings.uplink_feature_description
|
||||
|
||||
/**
|
||||
* At this firmware version periodic position sharing on a secondary channel was implemented. To enable this feature the
|
||||
* user must disable position on the primary channel and enable on a secondary channel. The lowest indexed secondary
|
||||
* channel with the position enabled will conduct the automatic position broadcasts.
|
||||
*/
|
||||
internal const val SECONDARY_CHANNEL_EPOCH = "2.6.10"
|
||||
@Composable
|
||||
internal fun ChannelLegend(onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable { onClick.invoke() },
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Row {
|
||||
Icon(imageVector = Icons.Filled.Info, contentDescription = stringResource(Res.string.info))
|
||||
Text(
|
||||
text = stringResource(Res.string.primary),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(Res.string.secondary),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class ChannelIcons(
|
||||
val icon: ImageVector,
|
||||
@@ -94,29 +108,7 @@ internal enum class ChannelIcons(
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ChannelLegend(onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable { onClick.invoke() },
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Row {
|
||||
Icon(imageVector = Icons.Filled.Info, contentDescription = stringResource(Res.string.info))
|
||||
Text(
|
||||
text = stringResource(Res.string.primary),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(Res.string.secondary),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ChannelLegendDialog(firmwareVersion: DeviceVersion, onDismiss: () -> Unit) {
|
||||
internal fun ChannelLegendDialog(capabilities: Capabilities, onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -148,7 +140,7 @@ internal fun ChannelLegendDialog(firmwareVersion: DeviceVersion, onDismiss: () -
|
||||
)
|
||||
Text(
|
||||
text =
|
||||
if (firmwareVersion >= DeviceVersion(asString = SECONDARY_CHANNEL_EPOCH)) {
|
||||
if (capabilities.supportsSecondaryChannelLocation) {
|
||||
/* 2.6.10+ */
|
||||
"- ${stringResource(Res.string.secondary_channel_position_feature)}"
|
||||
} else {
|
||||
@@ -192,5 +184,5 @@ private fun IconDefinitions() {
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewChannelLegendDialog() {
|
||||
ChannelLegendDialog(firmwareVersion = DeviceVersion(asString = SECONDARY_CHANNEL_EPOCH)) {}
|
||||
ChannelLegendDialog(capabilities = Capabilities("2.6.10")) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user