refactor: move RadioConfig files to separate package

This commit is contained in:
andrekir 2025-01-09 20:01:21 -03:00 committed by Andre K
parent 7794c08190
commit ad9a3a5e49
39 changed files with 501 additions and 357 deletions

View file

@ -54,7 +54,7 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.ui.components.config.ChannelSelection
import com.geeksville.mesh.ui.radioconfig.components.ChannelSelection
/**
* Enables the user to select which channels to accept after scanning a QR code.

View file

@ -1,148 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun AmbientLightingConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
AmbientLightingConfigItemList(
ambientLightingConfig = state.moduleConfig.ambientLighting,
enabled = state.connected,
onSaveClicked = { ambientLightingInput ->
val config = moduleConfig { ambientLighting = ambientLightingInput }
viewModel.setModuleConfig(config)
}
)
}
@Composable
fun AmbientLightingConfigItemList(
ambientLightingConfig: ModuleConfigProtos.ModuleConfig.AmbientLightingConfig,
enabled: Boolean,
onSaveClicked: (ModuleConfigProtos.ModuleConfig.AmbientLightingConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var ambientLightingInput by rememberSaveable { mutableStateOf(ambientLightingConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Ambient Lighting Config") }
item {
SwitchPreference(title = "LED state",
checked = ambientLightingInput.ledState,
enabled = enabled,
onCheckedChange = {
ambientLightingInput = ambientLightingInput.copy { ledState = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Current",
value = ambientLightingInput.current,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
ambientLightingInput = ambientLightingInput.copy { current = it }
})
}
item {
EditTextPreference(title = "Red",
value = ambientLightingInput.red,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { red = it } })
}
item {
EditTextPreference(title = "Green",
value = ambientLightingInput.green,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { green = it } })
}
item {
EditTextPreference(title = "Blue",
value = ambientLightingInput.blue,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { blue = it } })
}
item {
PreferenceFooter(
enabled = enabled && ambientLightingInput != ambientLightingConfig,
onCancelClicked = {
focusManager.clearFocus()
ambientLightingInput = ambientLightingConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(ambientLightingInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun AmbientLightingConfigPreview() {
AmbientLightingConfigItemList(
ambientLightingConfig = ModuleConfigProtos.ModuleConfig.AmbientLightingConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,164 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.AudioConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun AudioConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
AudioConfigItemList(
audioConfig = state.moduleConfig.audio,
enabled = state.connected,
onSaveClicked = { audioInput ->
val config = moduleConfig { audio = audioInput }
viewModel.setModuleConfig(config)
}
)
}
@Composable
fun AudioConfigItemList(
audioConfig: AudioConfig,
enabled: Boolean,
onSaveClicked: (AudioConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var audioInput by rememberSaveable { mutableStateOf(audioConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Audio Config") }
item {
SwitchPreference(title = "CODEC 2 enabled",
checked = audioInput.codec2Enabled,
enabled = enabled,
onCheckedChange = { audioInput = audioInput.copy { codec2Enabled = it } })
}
item { Divider() }
item {
EditTextPreference(title = "PTT pin",
value = audioInput.pttPin,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { pttPin = it } })
}
item {
DropDownPreference(title = "CODEC2 sample rate",
enabled = enabled,
items = AudioConfig.Audio_Baud.entries
.filter { it != AudioConfig.Audio_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = audioInput.bitrate,
onItemSelected = { audioInput = audioInput.copy { bitrate = it } })
}
item { Divider() }
item {
EditTextPreference(title = "I2S word select",
value = audioInput.i2SWs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SWs = it } })
}
item {
EditTextPreference(title = "I2S data in",
value = audioInput.i2SSd,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SSd = it } })
}
item {
EditTextPreference(title = "I2S data out",
value = audioInput.i2SDin,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SDin = it } })
}
item {
EditTextPreference(title = "I2S clock",
value = audioInput.i2SSck,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SSck = it } })
}
item {
PreferenceFooter(
enabled = enabled && audioInput != audioConfig,
onCancelClicked = {
focusManager.clearFocus()
audioInput = audioConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(audioInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun AudioConfigPreview() {
AudioConfigItemList(
audioConfig = AudioConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,136 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.BluetoothConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun BluetoothConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
BluetoothConfigItemList(
bluetoothConfig = state.radioConfig.bluetooth,
enabled = state.connected,
onSaveClicked = { bluetoothInput ->
val config = config { bluetooth = bluetoothInput }
viewModel.setConfig(config)
}
)
}
@Composable
fun BluetoothConfigItemList(
bluetoothConfig: BluetoothConfig,
enabled: Boolean,
onSaveClicked: (BluetoothConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var bluetoothInput by rememberSaveable { mutableStateOf(bluetoothConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Bluetooth Config") }
item {
SwitchPreference(title = "Bluetooth enabled",
checked = bluetoothInput.enabled,
enabled = enabled,
onCheckedChange = { bluetoothInput = bluetoothInput.copy { this.enabled = it } })
}
item { Divider() }
item {
DropDownPreference(title = "Pairing mode",
enabled = enabled,
items = BluetoothConfig.PairingMode.entries
.filter { it != BluetoothConfig.PairingMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = bluetoothInput.mode,
onItemSelected = { bluetoothInput = bluetoothInput.copy { mode = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Fixed PIN",
value = bluetoothInput.fixedPin,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it.toString().length == 6) { // ensure 6 digits
bluetoothInput = bluetoothInput.copy { fixedPin = it }
}
})
}
item {
PreferenceFooter(
enabled = enabled && bluetoothInput != bluetoothConfig,
onCancelClicked = {
focusManager.clearFocus()
bluetoothInput = bluetoothConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(bluetoothInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun BluetoothConfigPreview() {
BluetoothConfigItemList(
bluetoothConfig = BluetoothConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,256 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.CannedMessageConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun CannedMessageConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
CannedMessageConfigItemList(
messages = state.cannedMessageMessages,
cannedMessageConfig = state.moduleConfig.cannedMessage,
enabled = state.connected,
onSaveClicked = { messagesInput, cannedMessageInput ->
if (messagesInput != state.cannedMessageMessages) {
viewModel.setCannedMessages(messagesInput)
}
if (cannedMessageInput != state.moduleConfig.cannedMessage) {
val config = moduleConfig { cannedMessage = cannedMessageInput }
viewModel.setModuleConfig(config)
}
}
)
}
@Composable
fun CannedMessageConfigItemList(
messages: String,
cannedMessageConfig: CannedMessageConfig,
enabled: Boolean,
onSaveClicked: (messages: String, config: CannedMessageConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var messagesInput by rememberSaveable { mutableStateOf(messages) }
var cannedMessageInput by rememberSaveable { mutableStateOf(cannedMessageConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Canned Message Config") }
item {
SwitchPreference(title = "Canned message enabled",
checked = cannedMessageInput.enabled,
enabled = enabled,
onCheckedChange = {
cannedMessageInput = cannedMessageInput.copy { this.enabled = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Rotary encoder #1 enabled",
checked = cannedMessageInput.rotary1Enabled,
enabled = enabled,
onCheckedChange = {
cannedMessageInput = cannedMessageInput.copy { rotary1Enabled = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "GPIO pin for rotary encoder A port",
value = cannedMessageInput.inputbrokerPinA,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerPinA = it }
})
}
item {
EditTextPreference(title = "GPIO pin for rotary encoder B port",
value = cannedMessageInput.inputbrokerPinB,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerPinB = it }
})
}
item {
EditTextPreference(title = "GPIO pin for rotary encoder Press port",
value = cannedMessageInput.inputbrokerPinPress,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerPinPress = it }
})
}
item {
DropDownPreference(title = "Generate input event on Press",
enabled = enabled,
items = CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = cannedMessageInput.inputbrokerEventPress,
onItemSelected = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerEventPress = it }
})
}
item { Divider() }
item {
DropDownPreference(title = "Generate input event on CW",
enabled = enabled,
items = CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = cannedMessageInput.inputbrokerEventCw,
onItemSelected = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCw = it }
})
}
item { Divider() }
item {
DropDownPreference(title = "Generate input event on CCW",
enabled = enabled,
items = CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = cannedMessageInput.inputbrokerEventCcw,
onItemSelected = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCcw = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Up/Down/Select input enabled",
checked = cannedMessageInput.updown1Enabled,
enabled = enabled,
onCheckedChange = {
cannedMessageInput = cannedMessageInput.copy { updown1Enabled = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Allow input source",
value = cannedMessageInput.allowInputSource,
maxSize = 63, // allow_input_source max_size:16
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
cannedMessageInput = cannedMessageInput.copy { allowInputSource = it }
})
}
item {
SwitchPreference(title = "Send bell",
checked = cannedMessageInput.sendBell,
enabled = enabled,
onCheckedChange = {
cannedMessageInput = cannedMessageInput.copy { sendBell = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Messages",
value = messagesInput,
maxSize = 200, // messages max_size:201
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { messagesInput = it }
)
}
item {
PreferenceFooter(
enabled = enabled && cannedMessageInput != cannedMessageConfig || messagesInput != messages,
onCancelClicked = {
focusManager.clearFocus()
messagesInput = messages
cannedMessageInput = cannedMessageConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(messagesInput, cannedMessageInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun CannedMessageConfigPreview() {
CannedMessageConfigItemList(
messages = "",
cannedMessageConfig = CannedMessageConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { _, _ -> },
)
}

View file

@ -1,331 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Card
import androidx.compose.material.Checkbox
import androidx.compose.material.Chip
import androidx.compose.material.ContentAlpha
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Add
import androidx.compose.material.icons.twotone.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.R
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.dragContainer
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.components.rememberDragDropState
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun ChannelItem(
index: Int,
title: String,
enabled: Boolean,
onClick: () -> Unit = {},
elevation: Dp = 4.dp,
content: @Composable RowScope.() -> Unit,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp)
.clickable(enabled = enabled) { onClick() },
elevation = elevation,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp)
) {
val textColor = if (enabled) {
Color.Unspecified
} else {
MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
}
Chip(onClick = onClick) {
Text(
text = "$index",
color = textColor,
)
}
Text(
text = title,
modifier = Modifier.weight(1f),
color = textColor,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.body1,
)
content()
}
}
}
@Composable
fun ChannelCard(
index: Int,
title: String,
enabled: Boolean,
onEditClick: () -> Unit,
onDeleteClick: () -> Unit,
elevation: Dp = 4.dp,
) = ChannelItem(
index = index,
title = title,
enabled = enabled,
onClick = onEditClick,
elevation = elevation,
) {
IconButton(onClick = { onDeleteClick() }) {
Icon(
imageVector = Icons.TwoTone.Close,
contentDescription = stringResource(R.string.delete),
modifier = Modifier.wrapContentSize(),
)
}
}
@Composable
fun ChannelSelection(
index: Int,
title: String,
enabled: Boolean,
isSelected: Boolean,
onSelected: (Boolean) -> Unit
) = ChannelItem(
index = index,
title = title,
enabled = enabled,
onClick = {},
) {
Checkbox(
enabled = enabled,
checked = isSelected,
onCheckedChange = onSelected,
)
}
@Composable
fun ChannelConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
ChannelSettingsItemList(
settingsList = state.channelList,
modemPresetName = Channel(loraConfig = state.radioConfig.lora).name,
enabled = state.connected,
maxChannels = viewModel.maxChannels,
onPositiveClicked = { channelListInput ->
viewModel.updateChannels(channelListInput, state.channelList)
},
)
}
@Composable
fun ChannelSettingsItemList(
settingsList: List<ChannelSettings>,
modemPresetName: String = "Default",
maxChannels: Int = 8,
enabled: Boolean,
onNegativeClicked: () -> Unit = { },
onPositiveClicked: (List<ChannelSettings>) -> Unit,
) {
val focusManager = LocalFocusManager.current
val settingsListInput = rememberSaveable(
saver = listSaver(save = { it.toList() }, restore = { it.toMutableStateList() })
) { settingsList.toMutableStateList() }
val listState = rememberLazyListState()
val dragDropState = rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
if (toIndex in settingsListInput.indices && fromIndex in settingsListInput.indices) {
settingsListInput.apply { add(toIndex, removeAt(fromIndex)) }
}
}
val isEditing: Boolean = settingsList.size != settingsListInput.size ||
settingsList.zip(settingsListInput).any { (item1, item2) -> item1 != item2 }
var showEditChannelDialog: Int? by rememberSaveable { mutableStateOf(null) }
if (showEditChannelDialog != null) {
val index = showEditChannelDialog ?: return
EditChannelDialog(
channelSettings = with(settingsListInput) {
if (size > index) get(index) else channelSettings { }
},
modemPresetName = modemPresetName,
onAddClick = {
if (settingsListInput.size > index) {
settingsListInput[index] = it
} else {
settingsListInput.add(it)
}
showEditChannelDialog = null
},
onDismissRequest = { showEditChannelDialog = null }
)
}
Box(
modifier = Modifier
.fillMaxSize()
.clickable(onClick = { }, enabled = false)
) {
LazyColumn(
modifier = Modifier.dragContainer(
dragDropState = dragDropState,
haptics = LocalHapticFeedback.current,
),
state = listState,
contentPadding = PaddingValues(horizontal = 16.dp),
) {
item { PreferenceCategory(text = "Channels") }
dragDropItemsIndexed(
items = settingsListInput,
dragDropState = dragDropState,
) { index, channel, isDragging ->
val elevation by animateDpAsState(if (isDragging) 8.dp else 4.dp, label = "drag")
ChannelCard(
elevation = elevation,
index = index,
title = channel.name.ifEmpty { modemPresetName },
enabled = enabled,
onEditClick = { showEditChannelDialog = index },
onDeleteClick = { settingsListInput.removeAt(index) }
)
}
item {
PreferenceFooter(
enabled = enabled && isEditing,
negativeText = R.string.cancel,
onNegativeClicked = {
focusManager.clearFocus()
settingsListInput.clear()
settingsListInput.addAll(settingsList)
onNegativeClicked()
},
positiveText = R.string.send,
onPositiveClicked = {
focusManager.clearFocus()
onPositiveClicked(settingsListInput)
}
)
}
}
AnimatedVisibility(
visible = maxChannels > settingsListInput.size,
modifier = Modifier.align(Alignment.BottomEnd),
enter = slideInHorizontally(
initialOffsetX = { it },
animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing)
),
exit = slideOutHorizontally(
targetOffsetX = { it },
animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing)
)
) {
FloatingActionButton(
onClick = {
if (maxChannels > settingsListInput.size) {
settingsListInput.add(channelSettings {
psk = Channel.default.settings.psk
})
showEditChannelDialog = settingsListInput.lastIndex
}
},
modifier = Modifier.padding(16.dp)
) { Icon(Icons.TwoTone.Add, stringResource(R.string.add)) }
}
}
}
@Preview(showBackground = true)
@Composable
private fun ChannelSettingsPreview() {
ChannelSettingsItemList(
settingsList = listOf(
channelSettings {
psk = Channel.default.settings.psk
name = Channel.default.name
},
channelSettings {
name = stringResource(R.string.channel_name)
},
),
enabled = true,
onPositiveClicked = { },
)
}

View file

@ -1,199 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun DetectionSensorConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
DetectionSensorConfigItemList(
detectionSensorConfig = state.moduleConfig.detectionSensor,
enabled = state.connected,
onSaveClicked = { detectionSensorInput ->
val config = moduleConfig { detectionSensor = detectionSensorInput }
viewModel.setModuleConfig(config)
}
)
}
@Suppress("LongMethod")
@Composable
fun DetectionSensorConfigItemList(
detectionSensorConfig: ModuleConfig.DetectionSensorConfig,
enabled: Boolean,
onSaveClicked: (ModuleConfig.DetectionSensorConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var detectionSensorInput by rememberSaveable { mutableStateOf(detectionSensorConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Detection Sensor Config") }
item {
SwitchPreference(title = "Detection Sensor enabled",
checked = detectionSensorInput.enabled,
enabled = enabled,
onCheckedChange = {
detectionSensorInput = detectionSensorInput.copy { this.enabled = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Minimum broadcast (seconds)",
value = detectionSensorInput.minimumBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
detectionSensorInput = detectionSensorInput.copy { minimumBroadcastSecs = it }
})
}
item {
EditTextPreference(title = "State broadcast (seconds)",
value = detectionSensorInput.stateBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
detectionSensorInput = detectionSensorInput.copy { stateBroadcastSecs = it }
})
}
item {
SwitchPreference(title = "Send bell with alert message",
checked = detectionSensorInput.sendBell,
enabled = enabled,
onCheckedChange = {
detectionSensorInput = detectionSensorInput.copy { sendBell = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Friendly name",
value = detectionSensorInput.name,
maxSize = 19, // name max_size:20
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
detectionSensorInput = detectionSensorInput.copy { name = it }
})
}
item {
EditTextPreference(title = "GPIO pin to monitor",
value = detectionSensorInput.monitorPin,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
detectionSensorInput = detectionSensorInput.copy { monitorPin = it }
})
}
item {
DropDownPreference(
title = "Detection trigger type",
enabled = enabled,
items = ModuleConfig.DetectionSensorConfig.TriggerType.entries
.filter { it != ModuleConfig.DetectionSensorConfig.TriggerType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = detectionSensorInput.detectionTriggerType,
onItemSelected = {
detectionSensorInput = detectionSensorInput.copy { detectionTriggerType = it }
}
)
}
item { Divider() }
item {
SwitchPreference(title = "Use INPUT_PULLUP mode",
checked = detectionSensorInput.usePullup,
enabled = enabled,
onCheckedChange = {
detectionSensorInput = detectionSensorInput.copy { usePullup = it }
})
}
item { Divider() }
item {
PreferenceFooter(
enabled = enabled && detectionSensorInput != detectionSensorConfig,
onCancelClicked = {
focusManager.clearFocus()
detectionSensorInput = detectionSensorConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(detectionSensorInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun DetectionSensorConfigPreview() {
DetectionSensorConfigItemList(
detectionSensorConfig = ModuleConfig.DetectionSensorConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,239 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
import com.geeksville.mesh.R
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
private val DeviceConfig.Role.stringRes: Int
get() = when (this) {
DeviceConfig.Role.CLIENT -> R.string.role_client
DeviceConfig.Role.CLIENT_MUTE -> R.string.role_client_mute
DeviceConfig.Role.ROUTER -> R.string.role_router
DeviceConfig.Role.ROUTER_CLIENT -> R.string.role_router_client
DeviceConfig.Role.REPEATER -> R.string.role_repeater
DeviceConfig.Role.TRACKER -> R.string.role_tracker
DeviceConfig.Role.SENSOR -> R.string.role_sensor
DeviceConfig.Role.TAK -> R.string.role_tak
DeviceConfig.Role.CLIENT_HIDDEN -> R.string.role_client_hidden
DeviceConfig.Role.LOST_AND_FOUND -> R.string.role_lost_and_found
DeviceConfig.Role.TAK_TRACKER -> R.string.role_tak_tracker
DeviceConfig.Role.ROUTER_LATE -> R.string.role_router_late
else -> R.string.unrecognized
}
private val DeviceConfig.RebroadcastMode.stringRes: Int
get() = when (this) {
DeviceConfig.RebroadcastMode.ALL -> R.string.rebroadcast_mode_all
DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> R.string.rebroadcast_mode_all_skip_decoding
DeviceConfig.RebroadcastMode.LOCAL_ONLY -> R.string.rebroadcast_mode_local_only
DeviceConfig.RebroadcastMode.KNOWN_ONLY -> R.string.rebroadcast_mode_known_only
DeviceConfig.RebroadcastMode.NONE -> R.string.rebroadcast_mode_none
DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY -> R.string.rebroadcast_mode_core_portnums_only
else -> R.string.unrecognized
}
@Composable
fun DeviceConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
DeviceConfigItemList(
deviceConfig = state.radioConfig.device,
enabled = state.connected,
onSaveClicked = { deviceInput ->
val config = config { device = deviceInput }
viewModel.setConfig(config)
}
)
}
@Composable
fun DeviceConfigItemList(
deviceConfig: DeviceConfig,
enabled: Boolean,
onSaveClicked: (DeviceConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var deviceInput by rememberSaveable { mutableStateOf(deviceConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Device Config") }
item {
DropDownPreference(
title = "Role",
enabled = enabled,
selectedItem = deviceInput.role,
onItemSelected = { deviceInput = deviceInput.copy { role = it } },
summary = stringResource(id = deviceInput.role.stringRes),
)
Divider()
}
item {
EditTextPreference(title = "Redefine PIN_BUTTON",
value = deviceInput.buttonGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
deviceInput = deviceInput.copy { buttonGpio = it }
})
}
item {
EditTextPreference(title = "Redefine PIN_BUZZER",
value = deviceInput.buzzerGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
deviceInput = deviceInput.copy { buzzerGpio = it }
})
}
item {
DropDownPreference(
title = "Rebroadcast mode",
enabled = enabled,
selectedItem = deviceInput.rebroadcastMode,
onItemSelected = { deviceInput = deviceInput.copy { rebroadcastMode = it } },
summary = stringResource(id = deviceInput.rebroadcastMode.stringRes),
)
Divider()
}
item {
EditTextPreference(title = "NodeInfo broadcast interval (seconds)",
value = deviceInput.nodeInfoBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
deviceInput = deviceInput.copy { nodeInfoBroadcastSecs = it }
})
}
item {
SwitchPreference(
title = "Double tap as button press",
summary = stringResource(id = R.string.config_device_doubleTapAsButtonPress_summary),
checked = deviceInput.doubleTapAsButtonPress,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { doubleTapAsButtonPress = it } }
)
Divider()
}
item {
SwitchPreference(
title = "Disable triple-click",
summary = stringResource(id = R.string.config_device_disableTripleClick_summary),
checked = deviceInput.disableTripleClick,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { disableTripleClick = it } }
)
Divider()
}
item {
EditTextPreference(title = "POSIX Timezone",
value = deviceInput.tzdef,
maxSize = 64, // tzdef max_size:65
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
deviceInput = deviceInput.copy { tzdef = it }
},
)
}
item {
SwitchPreference(
title = "Disable LED heartbeat",
summary = stringResource(id = R.string.config_device_ledHeartbeatDisabled_summary),
checked = deviceInput.ledHeartbeatDisabled,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { ledHeartbeatDisabled = it } }
)
Divider()
}
item {
PreferenceFooter(
enabled = enabled && deviceInput != deviceConfig,
onCancelClicked = {
focusManager.clearFocus()
deviceInput = deviceConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(deviceInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun DeviceConfigPreview() {
DeviceConfigItemList(
deviceConfig = DeviceConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,210 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun DisplayConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
DisplayConfigItemList(
displayConfig = state.radioConfig.display,
enabled = state.connected,
onSaveClicked = { displayInput ->
val config = config { display = displayInput }
viewModel.setConfig(config)
}
)
}
@Composable
fun DisplayConfigItemList(
displayConfig: DisplayConfig,
enabled: Boolean,
onSaveClicked: (DisplayConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var displayInput by rememberSaveable { mutableStateOf(displayConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Display Config") }
item {
EditTextPreference(title = "Screen timeout (seconds)",
value = displayInput.screenOnSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { displayInput = displayInput.copy { screenOnSecs = it } })
}
item {
DropDownPreference(title = "GPS coordinates format",
enabled = enabled,
items = DisplayConfig.GpsCoordinateFormat.entries
.filter { it != DisplayConfig.GpsCoordinateFormat.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.gpsFormat,
onItemSelected = { displayInput = displayInput.copy { gpsFormat = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Auto screen carousel (seconds)",
value = displayInput.autoScreenCarouselSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
displayInput = displayInput.copy { autoScreenCarouselSecs = it }
})
}
item {
SwitchPreference(title = "Compass north top",
checked = displayInput.compassNorthTop,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { compassNorthTop = it } })
}
item { Divider() }
item {
SwitchPreference(title = "Flip screen",
checked = displayInput.flipScreen,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { flipScreen = it } })
}
item { Divider() }
item {
DropDownPreference(title = "Display units",
enabled = enabled,
items = DisplayConfig.DisplayUnits.entries
.filter { it != DisplayConfig.DisplayUnits.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.units,
onItemSelected = { displayInput = displayInput.copy { units = it } })
}
item { Divider() }
item {
DropDownPreference(title = "Override OLED auto-detect",
enabled = enabled,
items = DisplayConfig.OledType.entries
.filter { it != DisplayConfig.OledType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.oled,
onItemSelected = { displayInput = displayInput.copy { oled = it } })
}
item { Divider() }
item {
DropDownPreference(title = "Display mode",
enabled = enabled,
items = DisplayConfig.DisplayMode.entries
.filter { it != DisplayConfig.DisplayMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.displaymode,
onItemSelected = { displayInput = displayInput.copy { displaymode = it } })
}
item { Divider() }
item {
SwitchPreference(title = "Heading bold",
checked = displayInput.headingBold,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { headingBold = it } })
}
item { Divider() }
item {
SwitchPreference(title = "Wake screen on tap or motion",
checked = displayInput.wakeOnTapOrMotion,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { wakeOnTapOrMotion = it } })
}
item { Divider() }
item {
DropDownPreference(title = "Compass orientation",
enabled = enabled,
items = DisplayConfig.CompassOrientation.entries
.filter { it != DisplayConfig.CompassOrientation.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.compassOrientation,
onItemSelected = { displayInput = displayInput.copy { compassOrientation = it } })
}
item { Divider() }
item {
PreferenceFooter(
enabled = enabled && displayInput != displayConfig,
onCancelClicked = {
focusManager.clearFocus()
displayInput = displayConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(displayInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun DisplayConfigPreview() {
DisplayConfigItemList(
displayConfig = DisplayConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,178 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.ui.components.EditBase64Preference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PositionPrecisionPreference
import com.geeksville.mesh.ui.components.SwitchPreference
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun EditChannelDialog(
channelSettings: ChannelProtos.ChannelSettings,
onAddClick: (ChannelProtos.ChannelSettings) -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
modemPresetName: String = "Default",
) {
val focusManager = LocalFocusManager.current
var isFocused by remember { mutableStateOf(false) }
var channelInput by remember(channelSettings) { mutableStateOf(channelSettings) }
AlertDialog(
onDismissRequest = onDismissRequest,
shape = RoundedCornerShape(16.dp),
backgroundColor = MaterialTheme.colors.background,
text = {
Column(modifier.fillMaxWidth()) {
EditTextPreference(
title = stringResource(R.string.channel_name),
value = if (isFocused) channelInput.name else channelInput.name.ifEmpty { modemPresetName },
maxSize = 11, // name max_size:12
enabled = true,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
channelInput = channelInput.copy {
name = it.trim()
if (psk == Channel.default.settings.psk) psk = Channel.getRandomKey()
}
},
onFocusChanged = { isFocused = it.isFocused },
)
EditBase64Preference(
title = "PSK",
value = channelInput.psk,
enabled = true,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
val fullPsk = Channel(channelSettings { psk = it }).psk
if (fullPsk.size() in setOf(0, 16, 32)) {
channelInput = channelInput.copy { psk = it }
}
},
onGenerateKey = {
channelInput = channelInput.copy { psk = Channel.getRandomKey() }
},
)
SwitchPreference(
title = "Uplink enabled",
checked = channelInput.uplinkEnabled,
enabled = true,
onCheckedChange = {
channelInput = channelInput.copy { uplinkEnabled = it }
},
padding = PaddingValues(0.dp)
)
SwitchPreference(
title = "Downlink enabled",
checked = channelInput.downlinkEnabled,
enabled = true,
onCheckedChange = {
channelInput = channelInput.copy { downlinkEnabled = it }
},
padding = PaddingValues(0.dp)
)
PositionPrecisionPreference(
title = "Position enabled",
enabled = true,
value = channelInput.moduleSettings.positionPrecision,
onValueChanged = {
val module = channelInput.moduleSettings.copy { positionPrecision = it }
channelInput = channelInput.copy { moduleSettings = module }
},
)
}
},
buttons = {
FlowRow(
modifier = modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TextButton(
modifier = modifier.weight(1f),
onClick = onDismissRequest
) { Text(stringResource(R.string.cancel)) }
Button(
modifier = modifier.weight(1f),
onClick = {
onAddClick(channelInput)
},
enabled = true,
) { Text(stringResource(R.string.save)) }
}
}
)
}
@Preview(showBackground = true)
@Composable
private fun EditChannelDialogPreview() {
EditChannelDialog(
channelSettings = channelSettings {
psk = Channel.default.settings.psk
name = Channel.default.name
},
onAddClick = { },
onDismissRequest = { },
)
}

View file

@ -1,134 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.components.SwitchPreference
import com.google.protobuf.Descriptors
private const val SupportedFields = 7
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun EditDeviceProfileDialog(
title: String,
deviceProfile: DeviceProfile,
onConfirm: (DeviceProfile) -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
val state = remember {
val fields = deviceProfile.descriptorForType.fields
.filter { it.number < SupportedFields } // TODO add ringtone & canned messages
mutableStateMapOf<Descriptors.FieldDescriptor, Boolean>()
.apply { putAll(fields.associateWith(deviceProfile::hasField)) }
}
AlertDialog(
onDismissRequest = onDismiss,
shape = RoundedCornerShape(16.dp),
backgroundColor = MaterialTheme.colors.background,
text = {
Column(modifier.fillMaxWidth()) {
Text(
text = title,
style = MaterialTheme.typography.h6.copy(
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
)
Divider()
state.keys.sortedBy { it.number }.forEach { field ->
SwitchPreference(
title = field.name,
checked = state[field] == true,
enabled = deviceProfile.hasField(field),
onCheckedChange = { state[field] = it },
padding = PaddingValues(0.dp)
)
}
Divider()
}
},
buttons = {
FlowRow(
modifier = modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TextButton(
modifier = modifier.weight(1f),
onClick = onDismiss
) { Text(stringResource(R.string.cancel)) }
Button(
modifier = modifier.weight(1f),
onClick = {
val builder = DeviceProfile.newBuilder()
deviceProfile.allFields.forEach { (field, value) ->
if (state[field] == true) {
builder.setField(field, value)
}
}
onConfirm(builder.build())
},
enabled = state.values.any { it },
) { Text(stringResource(R.string.save)) }
}
}
)
}
@Preview(showBackground = true)
@Composable
private fun EditDeviceProfileDialogPreview() {
EditDeviceProfileDialog(
title = "Export configuration",
deviceProfile = DeviceProfile.getDefaultInstance(),
onConfirm = {},
onDismiss = {},
)
}

View file

@ -1,288 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.ExternalNotificationConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.ui.components.TextDividerPreference
@Composable
fun ExternalNotificationConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
ExternalNotificationConfigItemList(
ringtone = state.ringtone,
extNotificationConfig = state.moduleConfig.externalNotification,
enabled = state.connected,
onSaveClicked = { ringtoneInput, extNotificationInput ->
if (ringtoneInput != state.ringtone) {
viewModel.setRingtone(ringtoneInput)
}
if (extNotificationInput != state.moduleConfig.externalNotification) {
val config = moduleConfig { externalNotification = extNotificationInput }
viewModel.setModuleConfig(config)
}
}
)
}
@Composable
fun ExternalNotificationConfigItemList(
ringtone: String,
extNotificationConfig: ExternalNotificationConfig,
enabled: Boolean,
onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var ringtoneInput by rememberSaveable { mutableStateOf(ringtone) }
var externalNotificationInput by rememberSaveable { mutableStateOf(extNotificationConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "External Notification Config") }
item {
SwitchPreference(title = "External notification enabled",
checked = externalNotificationInput.enabled,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { this.enabled = it }
})
}
item { TextDividerPreference("Notifications on message receipt", enabled = enabled) }
item {
SwitchPreference(title = "Alert message LED",
checked = externalNotificationInput.alertMessage,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { alertMessage = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Alert message buzzer",
checked = externalNotificationInput.alertMessageBuzzer,
enabled = enabled,
onCheckedChange = {
externalNotificationInput =
externalNotificationInput.copy { alertMessageBuzzer = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Alert message vibra",
checked = externalNotificationInput.alertMessageVibra,
enabled = enabled,
onCheckedChange = {
externalNotificationInput =
externalNotificationInput.copy { alertMessageVibra = it }
})
}
item { TextDividerPreference("Notifications on alert/bell receipt", enabled = enabled) }
item {
SwitchPreference(title = "Alert bell LED",
checked = externalNotificationInput.alertBell,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { alertBell = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Alert bell buzzer",
checked = externalNotificationInput.alertBellBuzzer,
enabled = enabled,
onCheckedChange = {
externalNotificationInput =
externalNotificationInput.copy { alertBellBuzzer = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Alert bell vibra",
checked = externalNotificationInput.alertBellVibra,
enabled = enabled,
onCheckedChange = {
externalNotificationInput =
externalNotificationInput.copy { alertBellVibra = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Output LED (GPIO)",
value = externalNotificationInput.output,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { output = it }
})
}
if (externalNotificationInput.output != 0) item {
SwitchPreference(title = "Output LED active high",
checked = externalNotificationInput.active,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { active = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Output buzzer (GPIO)",
value = externalNotificationInput.outputBuzzer,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { outputBuzzer = it }
})
}
if (externalNotificationInput.outputBuzzer != 0) item {
SwitchPreference(title = "Use PWM buzzer",
checked = externalNotificationInput.usePwm,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { usePwm = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Output vibra (GPIO)",
value = externalNotificationInput.outputVibra,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { outputVibra = it }
})
}
item {
EditTextPreference(title = "Output duration (milliseconds)",
value = externalNotificationInput.outputMs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { outputMs = it }
})
}
item {
EditTextPreference(title = "Nag timeout (seconds)",
value = externalNotificationInput.nagTimeout,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { nagTimeout = it }
})
}
item {
EditTextPreference(title = "Ringtone",
value = ringtoneInput,
maxSize = 230, // ringtone max_size:231
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ringtoneInput = it }
)
}
item {
SwitchPreference(title = "Use I2S as buzzer",
checked = externalNotificationInput.useI2SAsBuzzer,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { useI2SAsBuzzer = it }
})
}
item { Divider() }
item {
PreferenceFooter(
enabled = enabled && externalNotificationInput != extNotificationConfig || ringtoneInput != ringtone,
onCancelClicked = {
focusManager.clearFocus()
ringtoneInput = ringtone
externalNotificationInput = extNotificationConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(ringtoneInput, externalNotificationInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun ExternalNotificationConfigPreview() {
ExternalNotificationConfigItemList(
ringtone = "",
extNotificationConfig = ExternalNotificationConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { _, _ -> },
)
}

View file

@ -1,287 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
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
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.model.RegionInfo
import com.geeksville.mesh.model.numChannels
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditListPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun LoRaConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
LoRaConfigItemList(
loraConfig = state.radioConfig.lora,
primarySettings = state.channelList.getOrNull(0) ?: return,
enabled = state.connected,
onSaveClicked = { loraInput ->
val config = config { lora = loraInput }
viewModel.setConfig(config)
},
hasPaFan = viewModel.hasPaFan,
)
}
@Suppress("LongMethod")
@Composable
fun LoRaConfigItemList(
loraConfig: LoRaConfig,
primarySettings: ChannelSettings,
enabled: Boolean,
onSaveClicked: (LoRaConfig) -> Unit,
hasPaFan: Boolean = false,
) {
val focusManager = LocalFocusManager.current
var loraInput by rememberSaveable { mutableStateOf(loraConfig) }
val primaryChannel by remember(loraInput) {
mutableStateOf(Channel(primarySettings, loraInput))
}
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "LoRa Config") }
item {
SwitchPreference(title = "Use modem preset",
checked = loraInput.usePreset,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { usePreset = it } })
}
item { Divider() }
if (loraInput.usePreset) {
item {
DropDownPreference(title = "Modem preset",
enabled = enabled && loraInput.usePreset,
items = LoRaConfig.ModemPreset.entries
.filter { it != LoRaConfig.ModemPreset.UNRECOGNIZED }
.map { it to it.name },
selectedItem = loraInput.modemPreset,
onItemSelected = { loraInput = loraInput.copy { modemPreset = it } })
}
item { Divider() }
} else {
item {
EditTextPreference(title = "Bandwidth",
value = loraInput.bandwidth,
enabled = enabled && !loraInput.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { bandwidth = it } })
}
item {
EditTextPreference(title = "Spread factor",
value = loraInput.spreadFactor,
enabled = enabled && !loraInput.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { spreadFactor = it } })
}
item {
EditTextPreference(title = "Coding rate",
value = loraInput.codingRate,
enabled = enabled && !loraInput.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { codingRate = it } })
}
}
item {
EditTextPreference(title = "Frequency offset (MHz)",
value = loraInput.frequencyOffset,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { frequencyOffset = it } })
}
item {
DropDownPreference(title = "Region (frequency plan)",
enabled = enabled,
items = RegionInfo.entries.map { it.regionCode to it.description },
selectedItem = loraInput.region,
onItemSelected = { loraInput = loraInput.copy { region = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Hop limit",
value = loraInput.hopLimit,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { hopLimit = it } })
}
item {
SwitchPreference(title = "TX enabled",
checked = loraInput.txEnabled,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { txEnabled = it } })
}
item { Divider() }
item {
EditTextPreference(title = "TX power (dBm)",
value = loraInput.txPower,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { txPower = it } })
}
item {
var isFocused by remember { mutableStateOf(false) }
EditTextPreference(title = "Frequency slot",
value = if (isFocused || loraInput.channelNum != 0) loraInput.channelNum else primaryChannel.channelNum,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onFocusChanged = { isFocused = it.isFocused },
onValueChanged = {
if (it <= loraInput.numChannels) { // total num of LoRa channels
loraInput = loraInput.copy { channelNum = it }
}
})
}
item {
SwitchPreference(title = "Override Duty Cycle",
checked = loraInput.overrideDutyCycle,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { overrideDutyCycle = it } })
}
item { Divider() }
item {
EditListPreference(title = "Ignore incoming",
list = loraInput.ignoreIncomingList,
maxCount = 3, // ignore_incoming max_count:3
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = { list ->
loraInput = loraInput.copy {
ignoreIncoming.clear()
ignoreIncoming.addAll(list.filter { it != 0 })
}
})
}
item {
SwitchPreference(title = "SX126X RX boosted gain",
checked = loraInput.sx126XRxBoostedGain,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { sx126XRxBoostedGain = it } })
}
item { Divider() }
item {
var isFocused by remember { mutableStateOf(false) }
EditTextPreference(title = "Override frequency (MHz)",
value = if (isFocused || loraInput.overrideFrequency != 0f) loraInput.overrideFrequency else primaryChannel.radioFreq,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onFocusChanged = { isFocused = it.isFocused },
onValueChanged = { loraInput = loraInput.copy { overrideFrequency = it } })
}
if (hasPaFan) {
item {
SwitchPreference(
title = "PA fan disabled",
checked = loraInput.paFanDisabled,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { paFanDisabled = it } })
}
item { Divider() }
}
item {
SwitchPreference(title = "Ignore MQTT",
checked = loraInput.ignoreMqtt,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { ignoreMqtt = it } })
}
item { Divider() }
item {
SwitchPreference(title = "OK to MQTT",
checked = loraInput.configOkToMqtt,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { configOkToMqtt = it } })
}
item { Divider() }
item {
PreferenceFooter(
enabled = enabled && loraInput != loraConfig,
onCancelClicked = {
focusManager.clearFocus()
loraInput = loraConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(loraInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun LoRaConfigPreview() {
LoRaConfigItemList(
loraConfig = Channel.default.loraConfig,
primarySettings = Channel.default.settings,
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,228 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.MQTTConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditPasswordPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PositionPrecisionPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun MQTTConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
MQTTConfigItemList(
mqttConfig = state.moduleConfig.mqtt,
enabled = state.connected,
onSaveClicked = { mqttInput ->
val config = moduleConfig { mqtt = mqttInput }
viewModel.setModuleConfig(config)
}
)
}
@Composable
fun MQTTConfigItemList(
mqttConfig: MQTTConfig,
enabled: Boolean,
onSaveClicked: (MQTTConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var mqttInput by rememberSaveable { mutableStateOf(mqttConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "MQTT Config") }
item {
SwitchPreference(title = "MQTT enabled",
checked = mqttInput.enabled,
enabled = enabled,
onCheckedChange = { mqttInput = mqttInput.copy { this.enabled = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Address",
value = mqttInput.address,
maxSize = 63, // address max_size:64
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { mqttInput = mqttInput.copy { address = it } })
}
item {
EditTextPreference(title = "Username",
value = mqttInput.username,
maxSize = 63, // username max_size:64
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { mqttInput = mqttInput.copy { username = it } })
}
item {
EditPasswordPreference(title = "Password",
value = mqttInput.password,
maxSize = 63, // password max_size:64
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { mqttInput = mqttInput.copy { password = it } })
}
item {
SwitchPreference(title = "Encryption enabled",
checked = mqttInput.encryptionEnabled,
enabled = enabled,
onCheckedChange = { mqttInput = mqttInput.copy { encryptionEnabled = it } })
}
item { Divider() }
item {
SwitchPreference(title = "JSON output enabled",
checked = mqttInput.jsonEnabled,
enabled = enabled,
onCheckedChange = { mqttInput = mqttInput.copy { jsonEnabled = it } })
}
item { Divider() }
item {
SwitchPreference(title = "TLS enabled",
checked = mqttInput.tlsEnabled,
enabled = enabled,
onCheckedChange = { mqttInput = mqttInput.copy { tlsEnabled = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Root topic",
value = mqttInput.root,
maxSize = 31, // root max_size:32
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { mqttInput = mqttInput.copy { root = it } })
}
item {
SwitchPreference(title = "Proxy to client enabled",
checked = mqttInput.proxyToClientEnabled,
enabled = enabled,
onCheckedChange = { mqttInput = mqttInput.copy { proxyToClientEnabled = it } })
}
item { Divider() }
item {
PositionPrecisionPreference(
title = "Map reporting",
enabled = enabled,
value = mqttInput.mapReportSettings.positionPrecision,
onValueChanged = {
val settings = mqttInput.mapReportSettings.copy { positionPrecision = it }
mqttInput = mqttInput.copy {
mapReportingEnabled = settings.positionPrecision > 0
mapReportSettings = settings
}
},
modifier = Modifier.padding(horizontal = 16.dp)
)
}
item { Divider() }
item {
EditTextPreference(title = "Map reporting interval (seconds)",
value = mqttInput.mapReportSettings.publishIntervalSecs,
enabled = enabled && mqttInput.mapReportingEnabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val settings = mqttInput.mapReportSettings.copy { publishIntervalSecs = it }
mqttInput = mqttInput.copy { mapReportSettings = settings }
},
)
}
item {
PreferenceFooter(
enabled = enabled && mqttInput != mqttConfig,
onCancelClicked = {
focusManager.clearFocus()
mqttInput = mqttConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(mqttInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun MQTTConfigPreview() {
MQTTConfigItemList(
mqttConfig = MQTTConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,139 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun NeighborInfoConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
NeighborInfoConfigItemList(
neighborInfoConfig = state.moduleConfig.neighborInfo,
enabled = state.connected,
onSaveClicked = { neighborInfoInput ->
val config = moduleConfig { neighborInfo = neighborInfoInput }
viewModel.setModuleConfig(config)
}
)
}
@Composable
fun NeighborInfoConfigItemList(
neighborInfoConfig: ModuleConfigProtos.ModuleConfig.NeighborInfoConfig,
enabled: Boolean,
onSaveClicked: (ModuleConfigProtos.ModuleConfig.NeighborInfoConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var neighborInfoInput by rememberSaveable { mutableStateOf(neighborInfoConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Neighbor Info Config") }
item {
SwitchPreference(title = "Neighbor Info enabled",
checked = neighborInfoInput.enabled,
enabled = enabled,
onCheckedChange = {
neighborInfoInput = neighborInfoInput.copy { this.enabled = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Update interval (seconds)",
value = neighborInfoInput.updateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
neighborInfoInput = neighborInfoInput.copy { updateInterval = it }
})
}
item {
SwitchPreference(
title = "Transmit over LoRa",
summary = stringResource(id = R.string.config_device_transmitOverLora_summary),
checked = neighborInfoInput.transmitOverLora,
enabled = enabled,
onCheckedChange = {
neighborInfoInput = neighborInfoInput.copy { transmitOverLora = it }
}
)
Divider()
}
item {
PreferenceFooter(
enabled = enabled && neighborInfoInput != neighborInfoConfig,
onCancelClicked = {
focusManager.clearFocus()
neighborInfoInput = neighborInfoConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(neighborInfoInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun NeighborInfoConfigPreview() {
NeighborInfoConfigItemList(
neighborInfoConfig = ModuleConfigProtos.ModuleConfig.NeighborInfoConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,312 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.NetworkConfig
import com.geeksville.mesh.R
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditIPv4Preference
import com.geeksville.mesh.ui.components.EditPasswordPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SimpleAlertDialog
import com.geeksville.mesh.ui.components.SwitchPreference
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
@Composable
private fun ScanErrorDialog(
onDismiss: () -> Unit = {}
) = SimpleAlertDialog(
title = R.string.error,
text = R.string.wifi_qr_code_error,
onDismiss = onDismiss,
)
@Composable
fun NetworkConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
NetworkConfigItemList(
hasWifi = state.metadata?.hasWifi ?: true,
hasEthernet = state.metadata?.hasEthernet ?: true,
networkConfig = state.radioConfig.network,
enabled = state.connected,
onSaveClicked = { networkInput ->
val config = config { network = networkInput }
viewModel.setConfig(config)
}
)
}
private fun extractWifiCredentials(qrCode: String) = Regex("""WIFI:S:(.*?);.*?P:(.*?);""")
.find(qrCode)?.destructured
?.let { (ssid, password) -> ssid to password } ?: (null to null)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun NetworkConfigItemList(
hasWifi: Boolean,
hasEthernet: Boolean,
networkConfig: NetworkConfig,
enabled: Boolean,
onSaveClicked: (NetworkConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var networkInput by rememberSaveable { mutableStateOf(networkConfig) }
var showScanErrorDialog: Boolean by rememberSaveable { mutableStateOf(false) }
if (showScanErrorDialog) {
ScanErrorDialog { showScanErrorDialog = false }
}
val barcodeLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
if (result.contents != null) {
val (ssid, psk) = extractWifiCredentials(result.contents)
if (ssid != null && psk != null) {
networkInput = networkInput.copy { wifiSsid = ssid; wifiPsk = psk }
} else {
showScanErrorDialog = true
}
}
}
fun zxingScan() {
val zxingScan = ScanOptions().apply {
setCameraId(0)
setPrompt("")
setBeepEnabled(false)
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
}
barcodeLauncher.launch(zxingScan)
}
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Network Config") }
item {
SwitchPreference(title = "WiFi enabled",
checked = networkInput.wifiEnabled,
enabled = enabled && hasWifi,
onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } })
Divider()
}
item {
EditTextPreference(title = "SSID",
value = networkInput.wifiSsid,
maxSize = 32, // wifi_ssid max_size:33
enabled = enabled && hasWifi,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
networkInput = networkInput.copy { wifiSsid = it }
})
}
item {
EditPasswordPreference(title = "PSK",
value = networkInput.wifiPsk,
maxSize = 64, // wifi_psk max_size:65
enabled = enabled && hasWifi,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } })
}
item {
Button(
onClick = { zxingScan() },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.height(48.dp),
enabled = enabled && hasWifi,
) {
Text(text = stringResource(R.string.wifi_qr_code_scan))
}
}
item {
SwitchPreference(title = "Ethernet enabled",
checked = networkInput.ethEnabled,
enabled = enabled && hasEthernet,
onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } })
Divider()
}
item {
EditTextPreference(title = "NTP server",
value = networkInput.ntpServer,
maxSize = 32, // ntp_server max_size:33
enabled = enabled,
isError = networkInput.ntpServer.isEmpty(),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
networkInput = networkInput.copy { ntpServer = it }
})
}
item {
EditTextPreference(title = "rsyslog server",
value = networkInput.rsyslogServer,
maxSize = 32, // rsyslog_server max_size:33
enabled = enabled,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
networkInput = networkInput.copy { rsyslogServer = it }
})
}
item {
DropDownPreference(title = "IPv4 mode",
enabled = enabled,
items = NetworkConfig.AddressMode.entries
.filter { it != NetworkConfig.AddressMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = networkInput.addressMode,
onItemSelected = { networkInput = networkInput.copy { addressMode = it } })
Divider()
}
item {
EditIPv4Preference(title = "IP",
value = networkInput.ipv4Config.ip,
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { ip = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
})
}
item {
EditIPv4Preference(title = "Gateway",
value = networkInput.ipv4Config.gateway,
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { gateway = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
})
}
item {
EditIPv4Preference(title = "Subnet",
value = networkInput.ipv4Config.subnet,
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { subnet = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
})
}
item {
EditIPv4Preference(title = "DNS",
value = networkInput.ipv4Config.dns,
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { dns = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
})
}
item {
PreferenceFooter(
enabled = enabled && networkInput != networkConfig,
onCancelClicked = {
focusManager.clearFocus()
networkInput = networkConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(networkInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun NetworkConfigPreview() {
NetworkConfigItemList(
hasWifi = true,
hasEthernet = true,
networkConfig = NetworkConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}
@Preview(showBackground = true)
@Composable
private fun QrCodeErrorDialogPreview() {
ScanErrorDialog()
}

View file

@ -1,106 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.ResponseState
@Composable
fun <T> PacketResponseStateDialog(
state: ResponseState<T>,
onDismiss: () -> Unit = {},
onComplete: () -> Unit = {},
) {
AlertDialog(
onDismissRequest = {},
shape = RoundedCornerShape(16.dp),
backgroundColor = MaterialTheme.colors.background,
title = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (state is ResponseState.Loading) {
val progress by animateFloatAsState(
targetValue = state.completed.toFloat() / state.total.toFloat(),
label = "progress",
)
Text("%.0f%%".format(progress * 100))
LinearProgressIndicator(
progress = progress,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
color = MaterialTheme.colors.onSurface,
)
if (state.total == state.completed) onComplete()
}
if (state is ResponseState.Success) {
Text(text = stringResource(id = R.string.delivery_confirmed))
}
if (state is ResponseState.Error) {
Text(text = stringResource(id = R.string.error), minLines = 2)
Text(text = state.error.asString())
}
}
},
buttons = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
onClick = onDismiss,
modifier = Modifier.padding(top = 16.dp)
) { Text(stringResource(R.string.close)) }
}
}
)
}
@Preview(showBackground = true)
@Composable
private fun PacketResponseStateDialogPreview() {
PacketResponseStateDialog(
state = ResponseState.Loading(
total = 17,
completed = 5,
),
)
}

View file

@ -1,144 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun PaxcounterConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
PaxcounterConfigItemList(
paxcounterConfig = state.moduleConfig.paxcounter,
enabled = state.connected,
onSaveClicked = { paxcounterConfigInput ->
val config = moduleConfig { paxcounter = paxcounterConfigInput }
viewModel.setModuleConfig(config)
}
)
}
@Composable
fun PaxcounterConfigItemList(
paxcounterConfig: ModuleConfigProtos.ModuleConfig.PaxcounterConfig,
enabled: Boolean,
onSaveClicked: (ModuleConfigProtos.ModuleConfig.PaxcounterConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var paxcounterInput by rememberSaveable { mutableStateOf(paxcounterConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Paxcounter Config") }
item {
SwitchPreference(title = "Paxcounter enabled",
checked = paxcounterInput.enabled,
enabled = enabled,
onCheckedChange = {
paxcounterInput = paxcounterInput.copy { this.enabled = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Update interval (seconds)",
value = paxcounterInput.paxcounterUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
paxcounterInput = paxcounterInput.copy { paxcounterUpdateInterval = it }
})
}
item {
EditTextPreference(title = "WiFi RSSI threshold (defaults to -80)",
value = paxcounterInput.wifiThreshold,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
paxcounterInput = paxcounterInput.copy { wifiThreshold = it }
})
}
item {
EditTextPreference(title = "BLE RSSI threshold (defaults to -80)",
value = paxcounterInput.bleThreshold,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
paxcounterInput = paxcounterInput.copy { bleThreshold = it }
})
}
item {
PreferenceFooter(
enabled = enabled && paxcounterInput != paxcounterConfig,
onCancelClicked = {
focusManager.clearFocus()
paxcounterInput = paxcounterConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(paxcounterInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun PaxcounterConfigPreview() {
PaxcounterConfigItemList(
paxcounterConfig = ModuleConfigProtos.ModuleConfig.PaxcounterConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,271 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.ConfigProtos.Config.PositionConfig
import com.geeksville.mesh.Position
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.BitwisePreference
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun PositionConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val node by viewModel.destNode.collectAsStateWithLifecycle()
val currentPosition = Position(
latitude = node?.latitude ?: 0.0,
longitude = node?.longitude ?: 0.0,
altitude = node?.position?.altitude ?: 0,
time = 1, // ignore time for fixed_position
)
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
PositionConfigItemList(
location = currentPosition,
positionConfig = state.radioConfig.position,
enabled = state.connected,
onSaveClicked = { locationInput, positionInput ->
if (positionInput.fixedPosition) {
if (locationInput != currentPosition) {
viewModel.setFixedPosition(locationInput)
}
} else {
if (state.radioConfig.position.fixedPosition) {
// fixed position changed from enabled to disabled
viewModel.removeFixedPosition()
}
}
val config = config { position = positionInput }
viewModel.setConfig(config)
}
)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun PositionConfigItemList(
location: Position,
positionConfig: PositionConfig,
enabled: Boolean,
onSaveClicked: (position: Position, config: PositionConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var locationInput by rememberSaveable { mutableStateOf(location) }
var positionInput by rememberSaveable { mutableStateOf(positionConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Position Config") }
item {
EditTextPreference(title = "Position broadcast interval (seconds)",
value = positionInput.positionBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
positionInput = positionInput.copy { positionBroadcastSecs = it }
})
}
item {
SwitchPreference(title = "Smart position enabled",
checked = positionInput.positionBroadcastSmartEnabled,
enabled = enabled,
onCheckedChange = {
positionInput = positionInput.copy { positionBroadcastSmartEnabled = it }
})
}
item { Divider() }
if (positionInput.positionBroadcastSmartEnabled) {
item {
EditTextPreference(title = "Smart broadcast minimum distance (meters)",
value = positionInput.broadcastSmartMinimumDistance,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
positionInput = positionInput.copy { broadcastSmartMinimumDistance = it }
})
}
item {
EditTextPreference(title = "Smart broadcast minimum interval (seconds)",
value = positionInput.broadcastSmartMinimumIntervalSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
positionInput = positionInput.copy { broadcastSmartMinimumIntervalSecs = it }
})
}
}
item {
SwitchPreference(title = "Use fixed position",
checked = positionInput.fixedPosition,
enabled = enabled,
onCheckedChange = { positionInput = positionInput.copy { fixedPosition = it } })
}
item { Divider() }
if (positionInput.fixedPosition) {
item {
EditTextPreference(title = "Latitude",
value = locationInput.latitude,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value >= -90 && value <= 90.0) {
locationInput = locationInput.copy(latitude = value)
}
})
}
item {
EditTextPreference(title = "Longitude",
value = locationInput.longitude,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value >= -180 && value <= 180.0) {
locationInput = locationInput.copy(longitude = value)
}
})
}
item {
EditTextPreference(title = "Altitude (meters)",
value = locationInput.altitude,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
locationInput = locationInput.copy(altitude = value)
})
}
}
item {
DropDownPreference(title = "GPS mode",
enabled = enabled,
items = ConfigProtos.Config.PositionConfig.GpsMode.entries
.filter { it != ConfigProtos.Config.PositionConfig.GpsMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = positionInput.gpsMode,
onItemSelected = { positionInput = positionInput.copy { gpsMode = it } })
}
item { Divider() }
item {
EditTextPreference(title = "GPS update interval (seconds)",
value = positionInput.gpsUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { gpsUpdateInterval = it } })
}
item {
BitwisePreference(title = "Position flags",
value = positionInput.positionFlags,
enabled = enabled,
items = ConfigProtos.Config.PositionConfig.PositionFlags.entries
.filter { it != PositionConfig.PositionFlags.UNSET && it != PositionConfig.PositionFlags.UNRECOGNIZED }
.map { it.number to it.name },
onItemSelected = { positionInput = positionInput.copy { positionFlags = it } }
)
}
item { Divider() }
item {
EditTextPreference(title = "Redefine GPS_RX_PIN",
value = positionInput.rxGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { rxGpio = it } })
}
item {
EditTextPreference(title = "Redefine GPS_TX_PIN",
value = positionInput.txGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { txGpio = it } })
}
item {
EditTextPreference(title = "Redefine PIN_GPS_EN",
value = positionInput.gpsEnGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { gpsEnGpio = it } })
}
item {
PreferenceFooter(
enabled = enabled && positionInput != positionConfig || locationInput != location,
onCancelClicked = {
focusManager.clearFocus()
locationInput = location
positionInput = positionConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(locationInput, positionInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun PositionConfigPreview() {
PositionConfigItemList(
location = Position(0.0, 0.0, 0),
positionConfig = PositionConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { _, _ -> },
)
}

View file

@ -1,170 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.PowerConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun PowerConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
PowerConfigItemList(
powerConfig = state.radioConfig.power,
enabled = state.connected,
onSaveClicked = { powerInput ->
val config = config { power = powerInput }
viewModel.setConfig(config)
}
)
}
@Composable
fun PowerConfigItemList(
powerConfig: PowerConfig,
enabled: Boolean,
onSaveClicked: (PowerConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var powerInput by rememberSaveable { mutableStateOf(powerConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Power Config") }
item {
SwitchPreference(title = "Enable power saving mode",
checked = powerInput.isPowerSaving,
enabled = enabled,
onCheckedChange = { powerInput = powerInput.copy { isPowerSaving = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Shutdown on battery delay (seconds)",
value = powerInput.onBatteryShutdownAfterSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
powerInput = powerInput.copy { onBatteryShutdownAfterSecs = it }
})
}
item {
EditTextPreference(title = "ADC multiplier override ratio",
value = powerInput.adcMultiplierOverride,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { adcMultiplierOverride = it } })
}
item {
EditTextPreference(title = "Wait for Bluetooth duration (seconds)",
value = powerInput.waitBluetoothSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { waitBluetoothSecs = it } })
}
item {
EditTextPreference(title = "Super deep sleep duration (seconds)",
value = powerInput.sdsSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { sdsSecs = it } })
}
item {
EditTextPreference(title = "Light sleep duration (seconds)",
value = powerInput.lsSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { lsSecs = it } })
}
item {
EditTextPreference(title = "Minimum wake time (seconds)",
value = powerInput.minWakeSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { minWakeSecs = it } })
}
item {
EditTextPreference(title = "Battery INA_2XX I2C address",
value = powerInput.deviceBatteryInaAddress,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { deviceBatteryInaAddress = it } })
}
item {
PreferenceFooter(
enabled = enabled && powerInput != powerConfig,
onCancelClicked = {
focusManager.clearFocus()
powerInput = powerConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(powerInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun PowerConfigPreview() {
PowerConfigItemList(
powerConfig = PowerConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,128 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RangeTestConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun RangeTestConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
RangeTestConfigItemList(
rangeTestConfig = state.moduleConfig.rangeTest,
enabled = state.connected,
onSaveClicked = { rangeTestInput ->
val config = moduleConfig { rangeTest = rangeTestInput }
viewModel.setModuleConfig(config)
}
)
}
@Composable
fun RangeTestConfigItemList(
rangeTestConfig: RangeTestConfig,
enabled: Boolean,
onSaveClicked: (RangeTestConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var rangeTestInput by rememberSaveable { mutableStateOf(rangeTestConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Range Test Config") }
item {
SwitchPreference(title = "Range test enabled",
checked = rangeTestInput.enabled,
enabled = enabled,
onCheckedChange = { rangeTestInput = rangeTestInput.copy { this.enabled = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Sender message interval (seconds)",
value = rangeTestInput.sender,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { rangeTestInput = rangeTestInput.copy { sender = it } })
}
item {
SwitchPreference(title = "Save .CSV in storage (ESP32 only)",
checked = rangeTestInput.save,
enabled = enabled,
onCheckedChange = { rangeTestInput = rangeTestInput.copy { save = it } })
}
item { Divider() }
item {
PreferenceFooter(
enabled = enabled && rangeTestInput != rangeTestConfig,
onCancelClicked = {
focusManager.clearFocus()
rangeTestInput = rangeTestConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(rangeTestInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun RangeTestConfig() {
RangeTestConfigItemList(
rangeTestConfig = RangeTestConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,138 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RemoteHardwareConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditListPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun RemoteHardwareConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
RemoteHardwareConfigItemList(
remoteHardwareConfig = state.moduleConfig.remoteHardware,
enabled = state.connected,
onSaveClicked = { remoteHardwareInput ->
val config = moduleConfig { remoteHardware = remoteHardwareInput }
viewModel.setModuleConfig(config)
}
)
}
@Composable
fun RemoteHardwareConfigItemList(
remoteHardwareConfig: RemoteHardwareConfig,
enabled: Boolean,
onSaveClicked: (RemoteHardwareConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var remoteHardwareInput by rememberSaveable { mutableStateOf(remoteHardwareConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Remote Hardware Config") }
item {
SwitchPreference(title = "Remote Hardware enabled",
checked = remoteHardwareInput.enabled,
enabled = enabled,
onCheckedChange = {
remoteHardwareInput = remoteHardwareInput.copy { this.enabled = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Allow undefined pin access",
checked = remoteHardwareInput.allowUndefinedPinAccess,
enabled = enabled,
onCheckedChange = {
remoteHardwareInput = remoteHardwareInput.copy { allowUndefinedPinAccess = it }
})
}
item { Divider() }
item {
EditListPreference(title = "Available pins",
list = remoteHardwareInput.availablePinsList,
maxCount = 4, // available_pins max_count:4
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = { list ->
remoteHardwareInput = remoteHardwareInput.copy {
availablePins.clear()
availablePins.addAll(list)
}
})
}
item {
PreferenceFooter(
enabled = enabled && remoteHardwareInput != remoteHardwareConfig,
onCancelClicked = {
focusManager.clearFocus()
remoteHardwareInput = remoteHardwareConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(remoteHardwareInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun RemoteHardwareConfigPreview() {
RemoteHardwareConfigItemList(
remoteHardwareConfig = RemoteHardwareConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,188 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.EditBase64Preference
import com.geeksville.mesh.ui.components.EditListPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun SecurityConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
SecurityConfigItemList(
securityConfig = state.radioConfig.security,
enabled = state.connected,
onConfirm = { securityInput ->
val config = config { security = securityInput }
viewModel.setConfig(config)
}
)
}
@Suppress("LongMethod")
@Composable
fun SecurityConfigItemList(
securityConfig: SecurityConfig,
enabled: Boolean,
onConfirm: (config: SecurityConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var securityInput by rememberSaveable { mutableStateOf(securityConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Security Config") }
item {
EditBase64Preference(
title = "Public Key",
value = securityInput.publicKey,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
if (it.size() == 32) {
securityInput = securityInput.copy { publicKey = it }
}
},
)
}
item {
EditBase64Preference(
title = "Private Key",
value = securityInput.privateKey,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
if (it.size() == 32) {
securityInput = securityInput.copy { privateKey = it }
}
},
)
}
item {
EditListPreference(
title = "Admin Key",
list = securityInput.adminKeyList,
maxCount = 3,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = {
securityInput = securityInput.copy {
adminKey.clear()
adminKey.addAll(it)
}
},
)
}
item {
SwitchPreference(title = "Managed Mode",
checked = securityInput.isManaged,
enabled = enabled && securityInput.adminKeyCount > 0,
onCheckedChange = {
securityInput = securityInput.copy { isManaged = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Serial console",
checked = securityInput.serialEnabled,
enabled = enabled,
onCheckedChange = { securityInput = securityInput.copy { serialEnabled = it } })
}
item { Divider() }
item {
SwitchPreference(title = "Debug log API enabled",
checked = securityInput.debugLogApiEnabled,
enabled = enabled,
onCheckedChange = {
securityInput = securityInput.copy { debugLogApiEnabled = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Legacy Admin channel",
checked = securityInput.adminChannelEnabled,
enabled = enabled,
onCheckedChange = {
securityInput = securityInput.copy { adminChannelEnabled = it }
})
}
item { Divider() }
item {
PreferenceFooter(
enabled = enabled && securityInput != securityConfig,
onCancelClicked = {
focusManager.clearFocus()
securityInput = securityConfig
},
onSaveClicked = {
focusManager.clearFocus()
onConfirm(securityInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun SecurityConfigPreview() {
SecurityConfigItemList(
securityConfig = SecurityConfig.getDefaultInstance(),
enabled = true,
onConfirm = {},
)
}

View file

@ -1,177 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.SerialConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.DropDownPreference
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun SerialConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
SerialConfigItemList(
serialConfig = state.moduleConfig.serial,
enabled = state.connected,
onSaveClicked = { serialInput ->
val config = moduleConfig { serial = serialInput }
viewModel.setModuleConfig(config)
}
)
}
@Composable
fun SerialConfigItemList(
serialConfig: SerialConfig,
enabled: Boolean,
onSaveClicked: (SerialConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var serialInput by rememberSaveable { mutableStateOf(serialConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Serial Config") }
item {
SwitchPreference(title = "Serial enabled",
checked = serialInput.enabled,
enabled = enabled,
onCheckedChange = { serialInput = serialInput.copy { this.enabled = it } })
}
item { Divider() }
item {
SwitchPreference(title = "Echo enabled",
checked = serialInput.echo,
enabled = enabled,
onCheckedChange = { serialInput = serialInput.copy { echo = it } })
}
item { Divider() }
item {
EditTextPreference(title = "RX",
value = serialInput.rxd,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { rxd = it } })
}
item {
EditTextPreference(title = "TX",
value = serialInput.txd,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { txd = it } })
}
item {
DropDownPreference(title = "Serial baud rate",
enabled = enabled,
items = SerialConfig.Serial_Baud.entries
.filter { it != SerialConfig.Serial_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = serialInput.baud,
onItemSelected = { serialInput = serialInput.copy { baud = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Timeout",
value = serialInput.timeout,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { timeout = it } })
}
item {
DropDownPreference(title = "Serial mode",
enabled = enabled,
items = SerialConfig.Serial_Mode.entries
.filter { it != SerialConfig.Serial_Mode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = serialInput.mode,
onItemSelected = { serialInput = serialInput.copy { mode = it } })
}
item { Divider() }
item {
SwitchPreference(title = "Override console serial port",
checked = serialInput.overrideConsoleSerialPort,
enabled = enabled,
onCheckedChange = {
serialInput = serialInput.copy { overrideConsoleSerialPort = it }
})
}
item { Divider() }
item {
PreferenceFooter(
enabled = enabled && serialInput != serialConfig,
onCancelClicked = {
focusManager.clearFocus()
serialInput = serialConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(serialInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun SerialConfigPreview() {
SerialConfigItemList(
serialConfig = SerialConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,159 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.StoreForwardConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun StoreForwardConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
StoreForwardConfigItemList(
storeForwardConfig = state.moduleConfig.storeForward,
enabled = state.connected,
onSaveClicked = { storeForwardInput ->
val config = moduleConfig { storeForward = storeForwardInput }
viewModel.setModuleConfig(config)
}
)
}
@Composable
fun StoreForwardConfigItemList(
storeForwardConfig: StoreForwardConfig,
enabled: Boolean,
onSaveClicked: (StoreForwardConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var storeForwardInput by rememberSaveable { mutableStateOf(storeForwardConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Store & Forward Config") }
item {
SwitchPreference(title = "Store & Forward enabled",
checked = storeForwardInput.enabled,
enabled = enabled,
onCheckedChange = {
storeForwardInput = storeForwardInput.copy { this.enabled = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Heartbeat",
checked = storeForwardInput.heartbeat,
enabled = enabled,
onCheckedChange = { storeForwardInput = storeForwardInput.copy { heartbeat = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Number of records",
value = storeForwardInput.records,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { storeForwardInput = storeForwardInput.copy { records = it } })
}
item {
EditTextPreference(title = "History return max",
value = storeForwardInput.historyReturnMax,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
storeForwardInput = storeForwardInput.copy { historyReturnMax = it }
})
}
item {
EditTextPreference(title = "History return window",
value = storeForwardInput.historyReturnWindow,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
storeForwardInput = storeForwardInput.copy { historyReturnWindow = it }
})
}
item {
SwitchPreference(
title = "Server",
checked = storeForwardInput.isServer,
enabled = enabled,
onCheckedChange = { storeForwardInput = storeForwardInput.copy { isServer = it } })
}
item { Divider() }
item {
PreferenceFooter(
enabled = enabled && storeForwardInput != storeForwardConfig,
onCancelClicked = {
focusManager.clearFocus()
storeForwardInput = storeForwardConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(storeForwardInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun StoreForwardConfigPreview() {
StoreForwardConfigItemList(
storeForwardConfig = StoreForwardConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,204 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.TelemetryConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun TelemetryConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
TelemetryConfigItemList(
telemetryConfig = state.moduleConfig.telemetry,
enabled = state.connected,
onSaveClicked = { telemetryInput ->
val config = moduleConfig { telemetry = telemetryInput }
viewModel.setModuleConfig(config)
}
)
}
@Composable
fun TelemetryConfigItemList(
telemetryConfig: TelemetryConfig,
enabled: Boolean,
onSaveClicked: (TelemetryConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var telemetryInput by rememberSaveable { mutableStateOf(telemetryConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "Telemetry Config") }
item {
EditTextPreference(title = "Device metrics update interval (seconds)",
value = telemetryInput.deviceUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
telemetryInput = telemetryInput.copy { deviceUpdateInterval = it }
})
}
item {
EditTextPreference(title = "Environment metrics update interval (seconds)",
value = telemetryInput.environmentUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
telemetryInput = telemetryInput.copy { environmentUpdateInterval = it }
})
}
item {
SwitchPreference(title = "Environment metrics module enabled",
checked = telemetryInput.environmentMeasurementEnabled,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { environmentMeasurementEnabled = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Environment metrics on-screen enabled",
checked = telemetryInput.environmentScreenEnabled,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { environmentScreenEnabled = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Environment metrics use Fahrenheit",
checked = telemetryInput.environmentDisplayFahrenheit,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { environmentDisplayFahrenheit = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Air quality metrics module enabled",
checked = telemetryInput.airQualityEnabled,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { airQualityEnabled = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Air quality metrics update interval (seconds)",
value = telemetryInput.airQualityInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
telemetryInput = telemetryInput.copy { airQualityInterval = it }
})
}
item {
SwitchPreference(title = "Power metrics module enabled",
checked = telemetryInput.powerMeasurementEnabled,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { powerMeasurementEnabled = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Power metrics update interval (seconds)",
value = telemetryInput.powerUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
telemetryInput = telemetryInput.copy { powerUpdateInterval = it }
})
}
item {
SwitchPreference(title = "Power metrics on-screen enabled",
checked = telemetryInput.powerScreenEnabled,
enabled = enabled,
onCheckedChange = {
telemetryInput = telemetryInput.copy { powerScreenEnabled = it }
})
}
item { Divider() }
item {
PreferenceFooter(
enabled = enabled && telemetryInput != telemetryConfig,
onCancelClicked = {
focusManager.clearFocus()
telemetryInput = telemetryConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(telemetryInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun TelemetryConfigPreview() {
TelemetryConfigItemList(
telemetryConfig = TelemetryConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { },
)
}

View file

@ -1,164 +0,0 @@
/*
* Copyright (c) 2025 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 com.geeksville.mesh.ui.components.config
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.model.getInitials
import com.geeksville.mesh.ui.components.EditTextPreference
import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.PreferenceFooter
import com.geeksville.mesh.ui.components.RegularPreference
import com.geeksville.mesh.ui.components.SwitchPreference
import com.geeksville.mesh.user
@Composable
fun UserConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
}
UserConfigItemList(
userConfig = state.userConfig,
enabled = true,
onSaveClicked = viewModel::setOwner,
)
}
@Composable
fun UserConfigItemList(
userConfig: MeshProtos.User,
enabled: Boolean,
onSaveClicked: (MeshProtos.User) -> Unit,
) {
val focusManager = LocalFocusManager.current
var userInput by rememberSaveable { mutableStateOf(userConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "User Config") }
item {
RegularPreference(title = "Node ID",
subtitle = userInput.id,
onClick = {})
}
item { Divider() }
item {
EditTextPreference(title = "Long name",
value = userInput.longName,
maxSize = 39, // long_name max_size:40
enabled = enabled,
isError = userInput.longName.isEmpty(),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
userInput = userInput.copy { longName = it }
if (getInitials(it).toByteArray().size <= 4) { // short_name max_size:5
userInput = userInput.copy { shortName = getInitials(it) }
}
})
}
item {
EditTextPreference(title = "Short name",
value = userInput.shortName,
maxSize = 4, // short_name max_size:5
enabled = enabled,
isError = userInput.shortName.isEmpty(),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { userInput = userInput.copy { shortName = it } })
}
item {
RegularPreference(title = "Hardware model",
subtitle = userInput.hwModel.name,
onClick = {})
}
item { Divider() }
item {
SwitchPreference(title = "Licensed amateur radio",
checked = userInput.isLicensed,
enabled = enabled,
onCheckedChange = { userInput = userInput.copy { isLicensed = it } })
}
item { Divider() }
item {
PreferenceFooter(
enabled = enabled && userInput != userConfig,
onCancelClicked = {
focusManager.clearFocus()
userInput = userConfig
}, onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(userInput)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun UserConfigPreview() {
UserConfigItemList(
userConfig = user {
id = "!a280d9c8"
longName = "Meshtastic d9c8"
shortName = "d9c8"
hwModel = MeshProtos.HardwareModel.RAK4631
isLicensed = false
},
enabled = true,
onSaveClicked = { },
)
}