add ModuleConfig settings (#526)

This commit is contained in:
Andre K
2022-11-22 22:01:37 -03:00
committed by GitHub
parent 36cb78a332
commit 689e7e7eca
25 changed files with 938 additions and 74 deletions

View File

@@ -761,13 +761,18 @@ class MainActivity : BaseActivity(), Logging {
handler.removeCallbacksAndMessages(null)
return true
}
R.id.advanced_settings -> {
val fragmentManager: FragmentManager = supportFragmentManager
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
val nameFragment = AdvancedSettingsFragment()
fragmentTransaction.add(R.id.mainActivityLayout, nameFragment)
fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
R.id.device_settings -> {
supportFragmentManager.beginTransaction()
.add(R.id.mainActivityLayout, DeviceSettingsFragment())
.addToBackStack(null)
.commit()
return true
}
R.id.module_settings -> {
supportFragmentManager.beginTransaction()
.add(R.id.mainActivityLayout, ModuleSettingsFragment())
.addToBackStack(null)
.commit()
return true
}
R.id.save_messages_csv -> {

View File

@@ -15,15 +15,18 @@ import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.*
import com.geeksville.mesh.ConfigProtos.Config
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.QuickChatActionRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.repository.datastore.ChannelSetRepository
import com.geeksville.mesh.repository.datastore.LocalConfigRepository
import com.geeksville.mesh.repository.datastore.ModuleConfigRepository
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.util.GPSFormat
import com.geeksville.mesh.util.positionToMeter
@@ -81,6 +84,7 @@ class UIViewModel @Inject constructor(
private val channelSetRepository: ChannelSetRepository,
private val packetRepository: PacketRepository,
private val localConfigRepository: LocalConfigRepository,
private val moduleConfigRepository: ModuleConfigRepository,
private val quickChatActionRepository: QuickChatActionRepository,
private val preferences: SharedPreferences
) : ViewModel(), Logging {
@@ -95,6 +99,10 @@ class UIViewModel @Inject constructor(
val localConfig: StateFlow<LocalConfig> = _localConfig
val config get() = _localConfig.value
private val _moduleConfig = MutableStateFlow<LocalModuleConfig>(LocalModuleConfig.getDefaultInstance())
val moduleConfig: StateFlow<LocalModuleConfig> = _moduleConfig
val module get() = _moduleConfig.value
private val _channels = MutableStateFlow(ChannelSet())
val channels: StateFlow<ChannelSet> = _channels
@@ -115,6 +123,9 @@ class UIViewModel @Inject constructor(
localConfigRepository.localConfigFlow.onEach { config ->
_localConfig.value = config
}.launchIn(viewModelScope)
moduleConfigRepository.moduleConfigFlow.onEach { config ->
_moduleConfig.value = config
}.launchIn(viewModelScope)
viewModelScope.launch {
quickChatActionRepository.getAllActions().collect { actions ->
_quickChatActions.value = actions
@@ -238,6 +249,7 @@ class UIViewModel @Inject constructor(
val isRouter: Boolean = config.device.role == Config.DeviceConfig.Role.ROUTER
// We consider hasWifi = ESP32
fun hasGPS() = myNodeInfo.value?.hasGPS == true
fun hasWifi() = myNodeInfo.value?.hasWifi == true
/// hardware info about our local device (can be null)
@@ -311,6 +323,50 @@ class UIViewModel @Inject constructor(
meshService?.setConfig(config.toByteArray())
}
inline fun updateMQTTConfig(crossinline body: (ModuleConfig.MQTTConfig) -> ModuleConfig.MQTTConfig) {
val data = body(module.mqtt)
setModuleConfig(moduleConfig { mqtt = data })
}
inline fun updateSerialConfig(crossinline body: (ModuleConfig.SerialConfig) -> ModuleConfig.SerialConfig) {
val data = body(module.serial)
setModuleConfig(moduleConfig { serial = data })
}
inline fun updateExternalNotificationConfig(crossinline body: (ModuleConfig.ExternalNotificationConfig) -> ModuleConfig.ExternalNotificationConfig) {
val data = body(module.externalNotification)
setModuleConfig(moduleConfig { externalNotification = data })
}
inline fun updateStoreForwardConfig(crossinline body: (ModuleConfig.StoreForwardConfig) -> ModuleConfig.StoreForwardConfig) {
val data = body(module.storeForward)
setModuleConfig(moduleConfig { storeForward = data })
}
inline fun updateRangeTestConfig(crossinline body: (ModuleConfig.RangeTestConfig) -> ModuleConfig.RangeTestConfig) {
val data = body(module.rangeTest)
setModuleConfig(moduleConfig { rangeTest = data })
}
inline fun updateTelemetryConfig(crossinline body: (ModuleConfig.TelemetryConfig) -> ModuleConfig.TelemetryConfig) {
val data = body(module.telemetry)
setModuleConfig(moduleConfig { telemetry = data })
}
inline fun updateCannedMessageConfig(crossinline body: (ModuleConfig.CannedMessageConfig) -> ModuleConfig.CannedMessageConfig) {
val data = body(module.cannedMessage)
setModuleConfig(moduleConfig { cannedMessage = data })
}
inline fun updateAudioConfig(crossinline body: (ModuleConfig.AudioConfig) -> ModuleConfig.AudioConfig) {
val data = body(module.audio)
setModuleConfig(moduleConfig { audio = data })
}
fun setModuleConfig(config: ModuleConfig) {
meshService?.setModuleConfig(config.toByteArray())
}
/// Convert the channels array to and from [AppOnlyProtos.ChannelSet]
private var _channelSet: AppOnlyProtos.ChannelSet
get() = channels.value.protobuf

View File

@@ -7,6 +7,7 @@ import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -34,6 +35,19 @@ object DataStoreModule {
)
}
@Singleton
@Provides
fun provideModuleConfigDataStore(@ApplicationContext appContext: Context): DataStore<LocalModuleConfig> {
return DataStoreFactory.create(
serializer = ModuleConfigSerializer,
produceFile = { appContext.dataStoreFile("module_config.pb") },
corruptionHandler = ReplaceFileCorruptionHandler(
produceNewData = { LocalModuleConfig.getDefaultInstance() }
),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
)
}
@Singleton
@Provides
fun provideChannelSetDataStore(@ApplicationContext appContext: Context): DataStore<ChannelSet> {

View File

@@ -2,13 +2,13 @@ package com.geeksville.mesh.repository.datastore
import androidx.datastore.core.DataStore
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.ConfigProtos.Config
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.asSharedFlow
import java.io.IOException
import javax.inject.Inject
@@ -30,20 +30,15 @@ class LocalConfigRepository @Inject constructor(
}
}
private val _sendDeviceConfigFlow = MutableSharedFlow<ConfigProtos.Config>()
val sendDeviceConfigFlow = _sendDeviceConfigFlow.asSharedFlow()
private suspend fun sendDeviceConfig(config: ConfigProtos.Config) {
debug("Sending device config!")
_sendDeviceConfigFlow.emit(config)
}
private val _setConfigFlow = MutableSharedFlow<Config>()
val setConfigFlow: SharedFlow<Config> = _setConfigFlow
/**
* Update LocalConfig and send ConfigProtos.Config Oneof to the radio
*/
suspend fun setRadioConfig(config: ConfigProtos.Config) {
suspend fun setConfig(config: Config) {
setLocalConfig(config)
sendDeviceConfig(config)
_setConfigFlow.emit(config)
}
suspend fun clearLocalConfig() {
@@ -55,7 +50,7 @@ class LocalConfigRepository @Inject constructor(
/**
* Update LocalConfig from each ConfigProtos.Config Oneof
*/
suspend fun setLocalConfig(config: ConfigProtos.Config) {
suspend fun setLocalConfig(config: Config) {
if (config.hasDevice()) setDeviceConfig(config.device)
if (config.hasPosition()) setPositionConfig(config.position)
if (config.hasPower()) setPowerConfig(config.power)
@@ -65,44 +60,44 @@ class LocalConfigRepository @Inject constructor(
if (config.hasBluetooth()) setBluetoothConfig(config.bluetooth)
}
private suspend fun setDeviceConfig(config: ConfigProtos.Config.DeviceConfig) {
private suspend fun setDeviceConfig(config: Config.DeviceConfig) {
localConfigStore.updateData { preference ->
preference.toBuilder().setDevice(config).build()
}
}
private suspend fun setPositionConfig(config: ConfigProtos.Config.PositionConfig) {
private suspend fun setPositionConfig(config: Config.PositionConfig) {
localConfigStore.updateData { preference ->
preference.toBuilder().setPosition(config).build()
}
}
private suspend fun setPowerConfig(config: ConfigProtos.Config.PowerConfig) {
private suspend fun setPowerConfig(config: Config.PowerConfig) {
localConfigStore.updateData { preference ->
preference.toBuilder().setPower(config).build()
}
}
private suspend fun setWifiConfig(config: ConfigProtos.Config.NetworkConfig) {
private suspend fun setWifiConfig(config: Config.NetworkConfig) {
localConfigStore.updateData { preference ->
preference.toBuilder().setNetwork(config).build()
}
}
private suspend fun setDisplayConfig(config: ConfigProtos.Config.DisplayConfig) {
private suspend fun setDisplayConfig(config: Config.DisplayConfig) {
localConfigStore.updateData { preference ->
preference.toBuilder().setDisplay(config).build()
}
}
private suspend fun setLoraConfig(config: ConfigProtos.Config.LoRaConfig) {
private suspend fun setLoraConfig(config: Config.LoRaConfig) {
localConfigStore.updateData { preference ->
preference.toBuilder().setLora(config).build()
}
channelSetRepository.setLoraConfig(config)
}
private suspend fun setBluetoothConfig(config: ConfigProtos.Config.BluetoothConfig) {
private suspend fun setBluetoothConfig(config: Config.BluetoothConfig) {
localConfigStore.updateData { preference ->
preference.toBuilder().setBluetooth(config).build()
}

View File

@@ -0,0 +1,100 @@
package com.geeksville.mesh.repository.datastore
import androidx.datastore.core.DataStore
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import java.io.IOException
import javax.inject.Inject
/**
* Class that handles saving and retrieving config settings
*/
class ModuleConfigRepository @Inject constructor(
private val moduleConfigStore: DataStore<LocalModuleConfig>,
) : Logging {
val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
errormsg("Error reading LocalConfig settings: ${exception.message}")
emit(LocalModuleConfig.getDefaultInstance())
} else {
throw exception
}
}
suspend fun clearLocalModuleConfig() {
moduleConfigStore.updateData { preference ->
preference.toBuilder().clear().build()
}
}
/**
* Update LocalModuleConfig from each ModuleConfigProtos.ModuleConfig Oneof
*/
suspend fun setLocalModuleConfig(config: ModuleConfig) {
if (config.hasMqtt()) setMQTTConfig(config.mqtt)
if (config.hasSerial()) setSerialConfig(config.serial)
if (config.hasExternalNotification()) setExternalNotificationConfig(config.externalNotification)
if (config.hasStoreForward()) setStoreForwardConfig(config.storeForward)
if (config.hasRangeTest()) setRangeTestConfig(config.rangeTest)
if (config.hasTelemetry()) setTelemetryConfig(config.telemetry)
if (config.hasCannedMessage()) setCannedMessageConfig(config.cannedMessage)
if (config.hasAudio()) setAudioConfig(config.audio)
}
private suspend fun setMQTTConfig(config: ModuleConfig.MQTTConfig) {
moduleConfigStore.updateData { preference ->
preference.toBuilder().setMqtt(config).build()
}
}
private suspend fun setSerialConfig(config: ModuleConfig.SerialConfig) {
moduleConfigStore.updateData { preference ->
preference.toBuilder().setSerial(config).build()
}
}
private suspend fun setExternalNotificationConfig(config: ModuleConfig.ExternalNotificationConfig) {
moduleConfigStore.updateData { preference ->
preference.toBuilder().setExternalNotification(config).build()
}
}
private suspend fun setStoreForwardConfig(config: ModuleConfig.StoreForwardConfig) {
moduleConfigStore.updateData { preference ->
preference.toBuilder().setStoreForward(config).build()
}
}
private suspend fun setRangeTestConfig(config: ModuleConfig.RangeTestConfig) {
moduleConfigStore.updateData { preference ->
preference.toBuilder().setRangeTest(config).build()
}
}
private suspend fun setTelemetryConfig(config: ModuleConfig.TelemetryConfig) {
moduleConfigStore.updateData { preference ->
preference.toBuilder().setTelemetry(config).build()
}
}
private suspend fun setCannedMessageConfig(config: ModuleConfig.CannedMessageConfig) {
moduleConfigStore.updateData { preference ->
preference.toBuilder().setCannedMessage(config).build()
}
}
private suspend fun setAudioConfig(config: ModuleConfig.AudioConfig) {
moduleConfigStore.updateData { preference ->
preference.toBuilder().setAudio(config).build()
}
}
suspend fun fetchInitialModuleConfig() = moduleConfigStore.data.first()
}

View File

@@ -0,0 +1,26 @@
package com.geeksville.mesh.repository.datastore
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.google.protobuf.InvalidProtocolBufferException
import java.io.InputStream
import java.io.OutputStream
/**
* Serializer for the [LocalModuleConfig] object defined in localonly.proto.
*/
@Suppress("BlockingMethodInNonBlockingContext")
object ModuleConfigSerializer : Serializer<LocalModuleConfig> {
override val defaultValue: LocalModuleConfig = LocalModuleConfig.getDefaultInstance()
override suspend fun readFrom(input: InputStream): LocalModuleConfig {
try {
return LocalModuleConfig.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: LocalModuleConfig, output: OutputStream) = t.writeTo(output)
}

View File

@@ -22,6 +22,7 @@ import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.repository.datastore.ChannelSetRepository
import com.geeksville.mesh.repository.datastore.LocalConfigRepository
import com.geeksville.mesh.repository.datastore.ModuleConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.repository.radio.BluetoothInterface
import com.geeksville.mesh.repository.radio.RadioInterfaceService
@@ -66,6 +67,9 @@ class MeshService : Service(), Logging {
@Inject
lateinit var localConfigRepository: LocalConfigRepository
@Inject
lateinit var moduleConfigRepository: ModuleConfigRepository
@Inject
lateinit var channelSetRepository: ChannelSetRepository
@@ -902,9 +906,16 @@ class MeshService : Service(), Logging {
}
}
private fun setLocalModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
serviceScope.handledLaunch {
moduleConfigRepository.setLocalModuleConfig(config)
}
}
private fun clearLocalConfig() {
serviceScope.handledLaunch {
localConfigRepository.clearLocalConfig()
moduleConfigRepository.clearLocalModuleConfig()
}
}
@@ -1120,16 +1131,16 @@ class MeshService : Service(), Logging {
setLocalConfig(config)
}
private fun handleModuleConfig(module: ModuleConfigProtos.ModuleConfig) {
debug("Received moduleConfig ${module.toOneLineString()}")
private fun handleModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
debug("Received moduleConfig ${config.toOneLineString()}")
val packetToSave = MeshLog(
UUID.randomUUID().toString(),
"ModuleConfig ${module.payloadVariantCase}",
"ModuleConfig ${config.payloadVariantCase}",
System.currentTimeMillis(),
module.toString()
config.toString()
)
insertMeshLog(packetToSave)
// setModuleConfig(config)
setLocalModuleConfig(config)
}
private fun handleChannel(ch: ChannelProtos.Channel) {
@@ -1275,6 +1286,7 @@ class MeshService : Service(), Logging {
serviceScope.handledLaunch {
channelSetRepository.clearChannelSet()
localConfigRepository.clearLocalConfig()
moduleConfigRepository.clearLocalModuleConfig()
}
}
@@ -1446,7 +1458,18 @@ class MeshService : Service(), Logging {
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
setConfig = config
})
setLocalConfig(config) // Update our cached copy
setLocalConfig(config) // Update our local copy
}
/** Send our current module config to the device
*/
private fun setModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
if (deviceVersion < minDeviceVersion) return
debug("Setting new module config!")
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
setModuleConfig = config
})
setLocalModuleConfig(config) // Update our local copy
}
/**
@@ -1667,6 +1690,11 @@ class MeshService : Service(), Logging {
setConfig(parsed)
}
override fun setModuleConfig(payload: ByteArray) = toRemoteExceptions {
val parsed = ModuleConfigProtos.ModuleConfig.parseFrom(payload)
setModuleConfig(parsed)
}
override fun setChannel(payload: ByteArray?) = toRemoteExceptions {
val parsed = ChannelProtos.Channel.parseFrom(payload)
setChannel(parsed)

View File

@@ -7,33 +7,31 @@ import android.view.ViewGroup
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.activityViewModels
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.databinding.AdvancedSettingsBinding
import com.geeksville.mesh.databinding.ComposeViewBinding
import com.geeksville.mesh.model.UIViewModel
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class AdvancedSettingsFragment : ScreenFragment("Advanced Settings"), Logging {
private var _binding: AdvancedSettingsBinding? = null
class DeviceSettingsFragment : ScreenFragment("Advanced Settings"), Logging {
private var _binding: ComposeViewBinding? = null
private val binding get() = _binding!!
private val model: UIViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = AdvancedSettingsBinding.inflate(inflater, container, false)
_binding = ComposeViewBinding.inflate(inflater, container, false)
.apply {
deviceConfig.apply {
composeView.apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MdcTheme {
PreferenceScreen(model)
DeviceSettingsItemList(model)
}
}
}

View File

@@ -32,7 +32,7 @@ import com.geeksville.mesh.ui.components.RegularPreference
import com.geeksville.mesh.ui.components.SwitchPreference
@Composable
fun PreferenceItemList(viewModel: UIViewModel) {
fun DeviceSettingsItemList(viewModel: UIViewModel) {
val focusManager = LocalFocusManager.current
val hasWifi = viewModel.hasWifi()

View File

@@ -0,0 +1,41 @@
package com.geeksville.mesh.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.activityViewModels
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.databinding.ComposeViewBinding
import com.geeksville.mesh.model.UIViewModel
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ModuleSettingsFragment : ScreenFragment("Module Settings"), Logging {
private var _binding: ComposeViewBinding? = null
private val binding get() = _binding!!
private val model: UIViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = ComposeViewBinding.inflate(inflater, container, false)
.apply {
composeView.apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MdcTheme {
ModuleSettingsItemList(model)
}
}
}
}
return binding.root
}
}

View File

@@ -0,0 +1,613 @@
package com.geeksville.mesh.ui
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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
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.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
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 ModuleSettingsItemList(viewModel: UIViewModel) {
val focusManager = LocalFocusManager.current
val connectionState by viewModel.connectionState.observeAsState()
val connected = connectionState == MeshService.ConnectionState.CONNECTED
val moduleConfig by viewModel.moduleConfig.collectAsState()
// Temporary [ModuleConfigProtos.ModuleConfig] state holders
var mqttInput by remember(moduleConfig.mqtt) { mutableStateOf(moduleConfig.mqtt) }
var serialInput by remember(moduleConfig.serial) { mutableStateOf(moduleConfig.serial) }
var externalNotificationInput by remember(moduleConfig.externalNotification) { mutableStateOf(moduleConfig.externalNotification) }
var storeForwardInput by remember(moduleConfig.storeForward) { mutableStateOf(moduleConfig.storeForward) }
var rangeTestInput by remember(moduleConfig.rangeTest) { mutableStateOf(moduleConfig.rangeTest) }
var telemetryInput by remember(moduleConfig.telemetry) { mutableStateOf(moduleConfig.telemetry) }
var cannedMessageInput by remember(moduleConfig.cannedMessage) { mutableStateOf(moduleConfig.cannedMessage) }
var audioInput by remember(moduleConfig.audio) { mutableStateOf(moduleConfig.audio) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "MQTT Config") }
item {
SwitchPreference(title = "MQTT enabled",
checked = mqttInput.enabled,
enabled = connected,
onCheckedChange = { mqttInput = mqttInput.copy { enabled = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Address",
value = mqttInput.address,
enabled = connected,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value.toByteArray().size <= 31) // address max_size:32
mqttInput = mqttInput.copy { address = value }
})
}
item {
EditTextPreference(title = "Username",
value = mqttInput.username,
enabled = connected,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value.toByteArray().size <= 31) // username max_size:32
mqttInput = mqttInput.copy { username = value }
})
}
item {
EditTextPreference(title = "Password",
value = mqttInput.password,
enabled = connected,
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value.toByteArray().size <= 31) // password max_size:32
mqttInput = mqttInput.copy { password = value }
})
}
item {
SwitchPreference(title = "Encryption enabled",
checked = mqttInput.encryptionEnabled,
enabled = connected,
onCheckedChange = { mqttInput = mqttInput.copy { encryptionEnabled = it } })
}
item { Divider() }
item {
SwitchPreference(title = "JSON output enabled",
checked = mqttInput.jsonEnabled,
enabled = connected,
onCheckedChange = { mqttInput = mqttInput.copy { jsonEnabled = it } })
}
item { Divider() }
item {
PreferenceFooter(
enabled = mqttInput != moduleConfig.mqtt,
onCancelClicked = {
focusManager.clearFocus()
mqttInput = moduleConfig.mqtt
},
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateMQTTConfig { mqttInput }
})
}
item { PreferenceCategory(text = "Serial Config") }
item {
SwitchPreference(title = "Serial enabled",
checked = serialInput.enabled,
enabled = connected,
onCheckedChange = { serialInput = serialInput.copy { enabled = it } })
}
item { Divider() }
item {
SwitchPreference(title = "Echo enabled",
checked = serialInput.echo,
enabled = connected,
onCheckedChange = { serialInput = serialInput.copy { echo = it } })
}
item { Divider() }
item {
EditTextPreference(title = "RX",
value = serialInput.rxd,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { rxd = it } })
}
item {
EditTextPreference(title = "TX",
value = serialInput.txd,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { txd = it } })
}
item {
DropDownPreference(title = "Serial baud rate",
enabled = connected,
items = ModuleConfig.SerialConfig.Serial_Baud.values()
.filter { it != ModuleConfig.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 = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { timeout = it } })
}
item {
DropDownPreference(title = "Serial baud rate",
enabled = connected,
items = ModuleConfig.SerialConfig.Serial_Mode.values()
.filter { it != ModuleConfig.SerialConfig.Serial_Mode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = serialInput.mode,
onItemSelected = { serialInput = serialInput.copy { mode = it } })
}
item { Divider() }
item {
PreferenceFooter(
enabled = serialInput != moduleConfig.serial,
onCancelClicked = {
focusManager.clearFocus()
serialInput = moduleConfig.serial
},
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateSerialConfig { serialInput }
})
}
item { PreferenceCategory(text = "External Notification Config") }
item {
SwitchPreference(title = "External Notification enabled",
checked = externalNotificationInput.enabled,
enabled = connected,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { enabled = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "Output milliseconds",
value = externalNotificationInput.outputMs,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { outputMs = it }
})
}
item {
EditTextPreference(title = "Output",
value = externalNotificationInput.output,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
externalNotificationInput = externalNotificationInput.copy { output = it }
})
}
item {
SwitchPreference(title = "Active",
checked = externalNotificationInput.active,
enabled = connected,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { active = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Alert message",
checked = externalNotificationInput.alertMessage,
enabled = connected,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { alertMessage = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Alert bell",
checked = externalNotificationInput.alertBell,
enabled = connected,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { alertBell = it }
})
}
item { Divider() }
item {
PreferenceFooter(
enabled = externalNotificationInput != moduleConfig.externalNotification,
onCancelClicked = {
focusManager.clearFocus()
externalNotificationInput = moduleConfig.externalNotification
},
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateExternalNotificationConfig { externalNotificationInput }
})
}
item { PreferenceCategory(text = "Store & Forward Config") }
item {
SwitchPreference(title = "Store & Forward enabled",
checked = storeForwardInput.enabled,
enabled = connected,
onCheckedChange = { storeForwardInput = storeForwardInput.copy { enabled = it } })
}
item { Divider() }
item {
SwitchPreference(title = "Heartbeat",
checked = storeForwardInput.heartbeat,
enabled = connected,
onCheckedChange = { storeForwardInput = storeForwardInput.copy { heartbeat = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Number of records",
value = storeForwardInput.records,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { storeForwardInput = storeForwardInput.copy { records = it } })
}
item {
EditTextPreference(title = "History return max",
value = storeForwardInput.historyReturnMax,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
storeForwardInput = storeForwardInput.copy { historyReturnMax = it }
})
}
item {
EditTextPreference(title = "History return window",
value = storeForwardInput.historyReturnWindow,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
storeForwardInput = storeForwardInput.copy { historyReturnWindow = it }
})
}
item {
PreferenceFooter(
enabled = storeForwardInput != moduleConfig.storeForward,
onCancelClicked = {
focusManager.clearFocus()
storeForwardInput = moduleConfig.storeForward
},
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateStoreForwardConfig { storeForwardInput }
})
}
item { PreferenceCategory(text = "Range Test Config") }
item {
SwitchPreference(title = "Range Test enabled",
checked = rangeTestInput.enabled,
enabled = connected,
onCheckedChange = { rangeTestInput = rangeTestInput.copy { enabled = it } })
}
item { Divider() }
item {
EditTextPreference(title = "Sender message interval",
value = rangeTestInput.sender,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { rangeTestInput = rangeTestInput.copy { sender = it } })
}
item {
SwitchPreference(title = "Save .CSV in storage (ESP32 only)",
checked = rangeTestInput.save,
enabled = connected,
onCheckedChange = { rangeTestInput = rangeTestInput.copy { save = it } })
}
item { Divider() }
item {
PreferenceFooter(
enabled = rangeTestInput != moduleConfig.rangeTest,
onCancelClicked = {
focusManager.clearFocus()
rangeTestInput = moduleConfig.rangeTest
},
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateRangeTestConfig { rangeTestInput }
})
}
item { PreferenceCategory(text = "Telemetry Config") }
item {
EditTextPreference(title = "Device metrics update interval",
value = telemetryInput.deviceUpdateInterval,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
telemetryInput = telemetryInput.copy { deviceUpdateInterval = it }
})
}
item {
EditTextPreference(title = "Environment metrics update interval",
value = telemetryInput.environmentUpdateInterval,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
telemetryInput = telemetryInput.copy { environmentUpdateInterval = it }
})
}
item {
SwitchPreference(title = "Environment metrics module enabled",
checked = telemetryInput.environmentMeasurementEnabled,
enabled = connected,
onCheckedChange = {
telemetryInput = telemetryInput.copy { environmentMeasurementEnabled = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Environment metrics on-screen enabled",
checked = telemetryInput.environmentScreenEnabled,
enabled = connected,
onCheckedChange = {
telemetryInput = telemetryInput.copy { environmentScreenEnabled = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Environment metrics use Fahrenheit",
checked = telemetryInput.environmentDisplayFahrenheit,
enabled = connected,
onCheckedChange = {
telemetryInput = telemetryInput.copy { environmentDisplayFahrenheit = it }
})
}
item { Divider() }
item {
PreferenceFooter(
enabled = telemetryInput != moduleConfig.telemetry,
onCancelClicked = {
focusManager.clearFocus()
telemetryInput = moduleConfig.telemetry
},
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateTelemetryConfig { telemetryInput }
})
}
item { PreferenceCategory(text = "Canned Message Config") }
item {
SwitchPreference(title = "Rotary encoder enabled",
checked = cannedMessageInput.rotary1Enabled,
enabled = connected,
onCheckedChange = {
cannedMessageInput = cannedMessageInput.copy { rotary1Enabled = it }
})
}
item { Divider() }
item {
EditTextPreference(title = "GPIO pin for rotary encoder A port",
value = cannedMessageInput.inputbrokerPinA,
enabled = connected,
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 = connected,
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 = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
cannedMessageInput = cannedMessageInput.copy { inputbrokerPinPress = it }
})
}
item {
DropDownPreference(title = "Generate input event on Press",
enabled = connected,
items = ModuleConfig.CannedMessageConfig.InputEventChar.values()
.filter { it != ModuleConfig.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 = connected,
items = ModuleConfig.CannedMessageConfig.InputEventChar.values()
.filter { it != ModuleConfig.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 = connected,
items = ModuleConfig.CannedMessageConfig.InputEventChar.values()
.filter { it != ModuleConfig.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 = connected,
onCheckedChange = {
cannedMessageInput = cannedMessageInput.copy { updown1Enabled = it }
})
}
item { Divider() }
item {
PreferenceFooter(
enabled = cannedMessageInput != moduleConfig.cannedMessage,
onCancelClicked = {
focusManager.clearFocus()
cannedMessageInput = moduleConfig.cannedMessage
},
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateCannedMessageConfig { cannedMessageInput }
})
}
item { PreferenceCategory(text = "Audio Config") }
item {
SwitchPreference(title = "CODEC 2 enabled",
checked = audioInput.codec2Enabled,
enabled = connected,
onCheckedChange = { audioInput = audioInput.copy { codec2Enabled = it } })
}
item { Divider() }
item {
EditTextPreference(title = "ADC where Microphone is connected",
value = audioInput.micChan,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { micChan = it } })
}
item {
EditTextPreference(title = "DAC where Speaker is connected",
value = audioInput.ampPin,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { ampPin = it } })
}
item {
EditTextPreference(title = "PTT pin",
value = audioInput.pttPin,
enabled = connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { pttPin = it } })
}
item {
DropDownPreference(title = "CODEC2 sample rate",
enabled = connected,
items = ModuleConfig.AudioConfig.Audio_Baud.values()
.filter { it != ModuleConfig.AudioConfig.Audio_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = audioInput.bitrate,
onItemSelected = { audioInput = audioInput.copy { bitrate = it } })
}
item { Divider() }
item {
PreferenceFooter(
enabled = audioInput != moduleConfig.audio,
onCancelClicked = {
focusManager.clearFocus()
audioInput = moduleConfig.audio
},
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateAudioConfig { audioInput }
})
}
}
}

View File

@@ -1,21 +0,0 @@
package com.geeksville.mesh.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.geeksville.mesh.model.UIViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.viewModelFactory
import com.geeksville.mesh.ui.theme.AppTheme
@Composable
fun PreferenceScreen(viewModel: UIViewModel = viewModel()) {
PreferenceItemList(viewModel)
}
//@Preview(showBackground = true)
//@Composable
//fun PreferencePreview() {
// AppTheme {
// PreferenceScreen(viewModel(factory = viewModelFactory { }))
// }
//}

View File

@@ -310,6 +310,14 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
} else updateNodeInfo()
}
model.moduleConfig.asLiveData().observe(viewLifecycleOwner) {
if (!model.isConnected()) {
val moduleCount = it.allFields.size
if (moduleCount > 0)
binding.scanStatusText.text = "Module config ($moduleCount / 8)"
} else updateNodeInfo()
}
model.channels.asLiveData().observe(viewLifecycleOwner) {
if (!model.isConnected()) it.protobuf.let { ch ->
if (!ch.hasLoraConfig() && ch.settingsCount > 0)