feat: upcoming support for tak and trafficmanagement configs, device hw (#4671)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-27 11:44:19 -06:00 committed by GitHub
parent a07992530c
commit b2b21e10e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 737 additions and 891 deletions

View file

@ -49,8 +49,11 @@ import org.meshtastic.core.resources.remote_hardware
import org.meshtastic.core.resources.serial
import org.meshtastic.core.resources.status_message
import org.meshtastic.core.resources.store_forward
import org.meshtastic.core.resources.tak
import org.meshtastic.core.resources.telemetry
import org.meshtastic.core.resources.traffic_management
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Config
import org.meshtastic.proto.DeviceMetadata
enum class ModuleRoute(
@ -59,6 +62,7 @@ enum class ModuleRoute(
val icon: ImageVector?,
val type: Int = 0,
val isSupported: (Capabilities) -> Boolean = { true },
val isApplicable: (Config.DeviceConfig.Role?) -> Boolean = { true },
) {
MQTT(Res.string.mqtt, SettingsRoutes.MQTT, Icons.Rounded.Cloud, AdminMessage.ModuleConfigType.MQTT_CONFIG.value),
SERIAL(
@ -140,18 +144,51 @@ enum class ModuleRoute(
AdminMessage.ModuleConfigType.STATUSMESSAGE_CONFIG.value,
isSupported = { it.supportsStatusMessage },
),
TRAFFIC_MANAGEMENT(
Res.string.traffic_management,
SettingsRoutes.TrafficManagement,
Icons.Rounded.Speed,
AdminMessage.ModuleConfigType.TRAFFICMANAGEMENT_CONFIG.value,
isSupported = { it.supportsTrafficManagementConfig },
),
TAK(
Res.string.tak,
SettingsRoutes.TAK,
Icons.Rounded.People,
AdminMessage.ModuleConfigType.TAK_CONFIG.value,
isSupported = { it.supportsTakConfig },
isApplicable = { it == Config.DeviceConfig.Role.TAK || it == Config.DeviceConfig.Role.TAK_TRACKER },
),
;
val bitfield: Int
get() = 1 shl ordinal
get() =
when (this) {
MQTT -> 0x0001
SERIAL -> 0x0002
EXT_NOTIFICATION -> 0x0004
STORE_FORWARD -> 0x0008
RANGE_TEST -> 0x0010
TELEMETRY -> 0x0020
CANNED_MESSAGE -> 0x0040
AUDIO -> 0x0080
REMOTE_HARDWARE -> 0x0100
NEIGHBOR_INFO -> 0x0200
AMBIENT_LIGHTING -> 0x0400
DETECTION_SENSOR -> 0x0800
PAXCOUNTER -> 0x1000
STATUS_MESSAGE -> 0x0000 // Not excludable yet
TRAFFIC_MANAGEMENT -> 0x0000 // Not excludable yet
TAK -> 0x0000 // Not excludable yet
}
companion object {
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ModuleRoute> {
fun filterExcludedFrom(metadata: DeviceMetadata?, role: Config.DeviceConfig.Role?): List<ModuleRoute> {
val capabilities = Capabilities(metadata?.firmware_version)
return entries.filter {
val excludedModules = metadata?.excluded_modules ?: 0
val isExcluded = (excludedModules and it.bitfield) != 0
!isExcluded && it.isSupported(capabilities)
!isExcluded && it.isSupported(capabilities) && it.isApplicable(role)
}
}
}

View file

@ -22,8 +22,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.CleaningServices
import androidx.compose.material.icons.rounded.Download
@ -97,13 +95,15 @@ fun RadioConfigItemList(
onNavigate: (Route) -> Unit,
) {
val enabled = state.connected && !state.responseState.isWaiting() && !isManaged
var modules by remember { mutableStateOf(ModuleRoute.filterExcludedFrom(state.metadata)) }
var modules by remember {
mutableStateOf(ModuleRoute.filterExcludedFrom(state.metadata, state.radioConfig.device?.role))
}
LaunchedEffect(excludedModulesUnlocked) {
LaunchedEffect(excludedModulesUnlocked, state.metadata, state.radioConfig.device?.role) {
if (excludedModulesUnlocked) {
modules = ModuleRoute.entries
} else {
modules = ModuleRoute.filterExcludedFrom(state.metadata)
modules = ModuleRoute.filterExcludedFrom(state.metadata, state.radioConfig.device?.role)
}
}

View file

@ -355,6 +355,8 @@ constructor(
detection_sensor = config.detection_sensor ?: state.moduleConfig.detection_sensor,
paxcounter = config.paxcounter ?: state.moduleConfig.paxcounter,
statusmessage = config.statusmessage ?: state.moduleConfig.statusmessage,
traffic_management = config.traffic_management ?: state.moduleConfig.traffic_management,
tak = config.tak ?: state.moduleConfig.tak,
),
)
}
@ -591,6 +593,8 @@ constructor(
lmc.detection_sensor?.let { setModuleConfig(ModuleConfig(detection_sensor = it)) }
lmc.paxcounter?.let { setModuleConfig(ModuleConfig(paxcounter = it)) }
lmc.statusmessage?.let { setModuleConfig(ModuleConfig(statusmessage = it)) }
lmc.traffic_management?.let { setModuleConfig(ModuleConfig(traffic_management = it)) }
lmc.tak?.let { setModuleConfig(ModuleConfig(tak = it)) }
}
meshService?.commitEditSettings(destNum)
}
@ -823,6 +827,9 @@ constructor(
detection_sensor = response.detection_sensor ?: state.moduleConfig.detection_sensor,
paxcounter = response.paxcounter ?: state.moduleConfig.paxcounter,
statusmessage = response.statusmessage ?: state.moduleConfig.statusmessage,
traffic_management =
response.traffic_management ?: state.moduleConfig.traffic_management,
tak = response.tak ?: state.moduleConfig.tak,
),
)
}

View file

@ -0,0 +1,80 @@
/*
* 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
* 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.feature.settings.radio.component
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.getColorFrom
import org.meshtastic.core.database.model.getStringResFrom
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.tak
import org.meshtastic.core.resources.tak_config
import org.meshtastic.core.resources.tak_role
import org.meshtastic.core.resources.tak_team
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Composable
fun TAKConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val takConfig = state.moduleConfig.tak ?: ModuleConfig.TAKConfig()
val formState = rememberConfigState(initialValue = takConfig)
LaunchedEffect(takConfig) { formState.value = takConfig }
RadioConfigScreenList(
title = stringResource(Res.string.tak),
onBack = onBack,
configState = formState,
enabled = state.connected,
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = ModuleConfig(tak = it)
viewModel.setModuleConfig(config)
},
) {
item {
TitledCard(title = stringResource(Res.string.tak_config)) {
DropDownPreference(
title = stringResource(Res.string.tak_team),
enabled = state.connected,
selectedItem = formState.value.team,
itemLabel = { stringResource(getStringResFrom(it)) },
itemColor = { Color(getColorFrom(it)) },
onItemSelected = { formState.value = formState.value.copy(team = it) },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(Res.string.tak_role),
enabled = state.connected,
selectedItem = formState.value.role,
itemLabel = { stringResource(getStringResFrom(it)) },
onItemSelected = { formState.value = formState.value.copy(role = it) },
)
}
}
}
}

View file

@ -0,0 +1,208 @@
/*
* 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
* 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.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.traffic_management
import org.meshtastic.core.resources.traffic_management_config
import org.meshtastic.core.resources.traffic_management_drop_unknown_enabled
import org.meshtastic.core.resources.traffic_management_enabled
import org.meshtastic.core.resources.traffic_management_exhaust_hop_position
import org.meshtastic.core.resources.traffic_management_exhaust_hop_telemetry
import org.meshtastic.core.resources.traffic_management_nodeinfo_direct_response
import org.meshtastic.core.resources.traffic_management_nodeinfo_direct_response_max_hops
import org.meshtastic.core.resources.traffic_management_position_dedup
import org.meshtastic.core.resources.traffic_management_position_min_interval
import org.meshtastic.core.resources.traffic_management_position_precision
import org.meshtastic.core.resources.traffic_management_rate_limit_enabled
import org.meshtastic.core.resources.traffic_management_rate_limit_max_packets
import org.meshtastic.core.resources.traffic_management_rate_limit_window
import org.meshtastic.core.resources.traffic_management_router_preserve_hops
import org.meshtastic.core.resources.traffic_management_unknown_packet_threshold
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
@Suppress("LongMethod")
@Composable
fun TrafficManagementConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val tmConfig = state.moduleConfig.traffic_management ?: ModuleConfig.TrafficManagementConfig()
val formState = rememberConfigState(initialValue = tmConfig)
val focusManager = LocalFocusManager.current
LaunchedEffect(tmConfig) { formState.value = tmConfig }
RadioConfigScreenList(
title = stringResource(Res.string.traffic_management),
onBack = onBack,
configState = formState,
enabled = state.connected,
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = ModuleConfig(traffic_management = it)
viewModel.setModuleConfig(config)
},
) {
item {
TitledCard(title = stringResource(Res.string.traffic_management_config)) {
SwitchPreference(
title = stringResource(Res.string.traffic_management_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy(enabled = it) },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(Res.string.traffic_management_position_dedup),
checked = formState.value.position_dedup_enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy(position_dedup_enabled = it) },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.traffic_management_position_precision),
value = formState.value.position_precision_bits,
enabled = state.connected,
keyboardActions =
KeyboardActions(
onNext = { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Down) },
),
onValueChanged = { formState.value = formState.value.copy(position_precision_bits = it) },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.traffic_management_position_min_interval),
value = formState.value.position_min_interval_secs,
enabled = state.connected,
keyboardActions =
KeyboardActions(
onNext = { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Down) },
),
onValueChanged = { formState.value = formState.value.copy(position_min_interval_secs = it) },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(Res.string.traffic_management_nodeinfo_direct_response),
checked = formState.value.nodeinfo_direct_response,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy(nodeinfo_direct_response = it) },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.traffic_management_nodeinfo_direct_response_max_hops),
value = formState.value.nodeinfo_direct_response_max_hops,
enabled = state.connected,
keyboardActions =
KeyboardActions(
onNext = { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Down) },
),
onValueChanged = { formState.value = formState.value.copy(nodeinfo_direct_response_max_hops = it) },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(Res.string.traffic_management_rate_limit_enabled),
checked = formState.value.rate_limit_enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy(rate_limit_enabled = it) },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.traffic_management_rate_limit_window),
value = formState.value.rate_limit_window_secs,
enabled = state.connected,
keyboardActions =
KeyboardActions(
onNext = { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Down) },
),
onValueChanged = { formState.value = formState.value.copy(rate_limit_window_secs = it) },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.traffic_management_rate_limit_max_packets),
value = formState.value.rate_limit_max_packets,
enabled = state.connected,
keyboardActions =
KeyboardActions(
onNext = { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Down) },
),
onValueChanged = { formState.value = formState.value.copy(rate_limit_max_packets = it) },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(Res.string.traffic_management_drop_unknown_enabled),
checked = formState.value.drop_unknown_enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy(drop_unknown_enabled = it) },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.traffic_management_unknown_packet_threshold),
value = formState.value.unknown_packet_threshold,
enabled = state.connected,
keyboardActions =
KeyboardActions(
onNext = { focusManager.moveFocus(androidx.compose.ui.focus.FocusDirection.Down) },
),
onValueChanged = { formState.value = formState.value.copy(unknown_packet_threshold = it) },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(Res.string.traffic_management_exhaust_hop_telemetry),
checked = formState.value.exhaust_hop_telemetry,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy(exhaust_hop_telemetry = it) },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(Res.string.traffic_management_exhaust_hop_position),
checked = formState.value.exhaust_hop_position,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy(exhaust_hop_position = it) },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(Res.string.traffic_management_router_preserve_hops),
checked = formState.value.router_preserve_hops,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy(router_preserve_hops = it) },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
}
}