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

@@ -75,6 +75,10 @@ interface IMeshService {
/// It sets a Config protobuf via admin packet
void setConfig(in byte []payload);
/// This method is only intended for use in our GUI, so the user can set radio options
/// It sets a ModuleConfig protobuf via admin packet
void setModuleConfig(in byte []payload);
/// This method is only intended for use in our GUI, so the user can set radio options
/// It sets a Channel protobuf via admin packet
void setChannel(in byte []payload);

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)

View File

@@ -7,7 +7,7 @@
android:background="@color/colorAdvancedBackground">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/device_config"
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />

View File

@@ -17,9 +17,13 @@
android:checked="false"
android:title="@string/protocol_stress_test" />
<item
android:id="@+id/advanced_settings"
android:id="@+id/device_settings"
app:showAsAction="withText"
android:title="@string/advanced_settings" />
android:title="@string/device_settings" />
<item
android:id="@+id/module_settings"
app:showAsAction="withText"
android:title="@string/module_settings" />
<item
android:id="@+id/save_messages_csv"
app:showAsAction="withText"

View File

@@ -76,7 +76,6 @@
<string name="ls_sleep_secs">Período de reposo del dispositivo (en segundos)</string>
<string name="meshtastic_messages_notifications">Notificaciones de mensajes</string>
<string name="protocol_stress_test">Protocolo de prueba de esfuerzo</string>
<string name="advanced_settings">Configuración avanzada</string>
<string name="firmware_too_old">Es necesario actualizar el firmware</string>
<string name="okay">Vale</string>
<string name="must_set_region">¡Debe establecer una región!</string>

View File

@@ -76,7 +76,6 @@
<string name="ls_sleep_secs">Eszköz alvásának gyakorisága (másodpercben)</string>
<string name="meshtastic_messages_notifications">Értesítések az üzenetekről</string>
<string name="protocol_stress_test">Protokoll stressz teszt</string>
<string name="advanced_settings">Haladó beállítások</string>
<string name="firmware_too_old">Firmware frissítés szükséges</string>
<string name="firmware_old">A rádió firmware túl régi ahhoz, hogy a programmal kommunikálni tudjon, kérem menjen a beállítások oldalra és válassza a "firmware frissítés" gombot. További tudnivalókat a <a href="https://github.com/meshtastic/Meshtastic-device#firmware-installation">firmware frissítés leírásában</a> talál, a Github-on.</string>
<string name="okay">OK</string>

View File

@@ -82,7 +82,6 @@
<string name="ls_sleep_secs">기기가 절전모드 들어가기까지의 시간 (in seconds)</string>
<string name="meshtastic_messages_notifications">메시지 알림</string>
<string name="protocol_stress_test">프로토콜 스트레스 테스트</string>
<string name="advanced_settings">고급 설정</string>
<string name="firmware_too_old">펌웨어 업데이트 필요</string>
<string name="firmware_old">이 기기의 펌웨어가 매우 오래되어 이 앱과 호환되지않습니다, 설정에서 "펌웨어 업데이트"를 선택하여주세요. 더 자세한정보는 깃허브의 <a href="https://github.com/meshtastic/Meshtastic-device#firmware-installation"> </a> 에서 펌웨어 업데이트 가이드를 참고해주세요..</string>
<string name="okay">확인</string>

View File

@@ -77,7 +77,6 @@
<string name="ls_sleep_secs">Okres uśpienia urządzenia (w sekundach)</string>
<string name="meshtastic_messages_notifications">Powiadomienia o wiadomościach</string>
<string name="protocol_stress_test">Protokół testu warunków skrajnych</string>
<string name="advanced_settings">Zaawansowane ustawienia</string>
<string name="firmware_too_old">Wymagana aktualizacja oprogramowania układowego</string>
<string name="firmware_old">Oprogramowanie układowe radia jest zbyt stare, aby rozmawiać z tą aplikacją, przejdź do panelu ustawień i wybierz „Aktualizuj oprogramowanie układowe”. Aby uzyskać więcej informacji na ten temat, zobacz <a href="https://github.com/meshtastic/Meshtastic-device#firmware-installation">nasz przewodnik instalacji oprogramowania układowego</a> na Github.</string>
<string name="okay">OK</string>

View File

@@ -77,7 +77,6 @@
<string name="ls_sleep_secs">Intervalo de suspensão (sleep) (em segundos)</string>
<string name="meshtastic_messages_notifications">Notificações sobre mensagens</string>
<string name="protocol_stress_test">Stress test do protocolo</string>
<string name="advanced_settings">Configurações avançadas</string>
<string name="firmware_too_old">Atualização do firmware necessária</string>
<string name="firmware_old">Versão de firmware do rádio muito antiga para comunicar com este aplicativo, favor acessar opção "Atualizar firmware" nas Configurações. Para mais informações consultar <a href="https://github.com/meshtastic/Meshtastic-device#firmware-installation">Nosso guia de instalação de firmware</a> no Github.</string>
<string name="okay">Okay</string>

View File

@@ -77,7 +77,6 @@
<string name="ls_sleep_secs">Intervalo de suspensão (sleep) (segundos)</string>
<string name="meshtastic_messages_notifications">Notificações sobre mensagens</string>
<string name="protocol_stress_test">Stress test do protocolo</string>
<string name="advanced_settings">Configurações avançadas</string>
<string name="firmware_too_old">Atualização do firmware necessária</string>
<string name="firmware_old">Versão de firmware do rádio muito antiga para comunicar com este aplicativo, favor acessar opção "Atualizar firmware" nas Configurações. Para mais informações consultar <a href="https://github.com/meshtastic/Meshtastic-device#firmware-installation">Nosso guia de instalação de firmware</a> no Github.</string>
<string name="okay">Okay</string>

View File

@@ -77,7 +77,6 @@
<string name="ls_sleep_secs">Interval uspávania zariadenia (v sekundách)</string>
<string name="meshtastic_messages_notifications">Upozornenia na správy</string>
<string name="protocol_stress_test">Stres test protokolu</string>
<string name="advanced_settings">Rozšírené nastavenia</string>
<string name="firmware_too_old">Nutná aktualizácia firmvéru vysielača</string>
<string name="okay">Ok</string>
<string name="must_set_region">Musíte nastaviť región!</string>

View File

@@ -77,7 +77,6 @@
<string name="ls_sleep_secs">设备休眠时间(以秒为单位)</string>
<string name="meshtastic_messages_notifications">关于消息的通知</string>
<string name="protocol_stress_test">协议压力测试</string>
<string name="advanced_settings">高级设置</string>
<string name="firmware_too_old">需要固件更新</string>
<string name="firmware_old">固件太旧,无法与此应用程序对话,请转到设置窗头并选择"升级固件". 有关这方面的更多信息,请参阅<a href="https://github.com/meshtastic/Meshtastic-device#firmware-installation">固件安装指南</a> 在GitHub</string>
<string name="okay">好的</string>

View File

@@ -81,7 +81,6 @@
<string name="ls_sleep_secs">Device sleep period (in seconds)</string>
<string name="meshtastic_messages_notifications">Notifications about messages</string>
<string name="protocol_stress_test">Protocol stress test</string>
<string name="advanced_settings">Advanced settings</string>
<string name="firmware_too_old">Firmware update required</string>
<string name="firmware_old">The radio firmware is too old to talk to this application, please go to the settings pane and choose "Update Firmware". For more information on this see <a href="https://github.com/meshtastic/Meshtastic-device#firmware-installation">our Firmware Installation guide</a> on Github.</string>
<string name="okay">Okay</string>
@@ -163,4 +162,6 @@
<string name="start_download">Start Download</string>
<string name="request_position">Request position</string>
<string name="close">Close</string>
<string name="device_settings">Device settings</string>
<string name="module_settings">Module settings</string>
</resources>