mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
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:
parent
a07992530c
commit
b2b21e10e2
33 changed files with 737 additions and 891 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue