diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index 532a1510d..f91d5184c 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -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); diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 0cf59c45c..bf5e7ee16 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -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 -> { diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index fdb44a647..17494fb53 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -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 val config get() = _localConfig.value + private val _moduleConfig = MutableStateFlow(LocalModuleConfig.getDefaultInstance()) + val moduleConfig: StateFlow = _moduleConfig + val module get() = _moduleConfig.value + private val _channels = MutableStateFlow(ChannelSet()) val channels: StateFlow = _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 diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt index 069f26175..f979a6b3a 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt @@ -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 { + 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 { diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt index a62f187d4..16fb92318 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt @@ -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() - val sendDeviceConfigFlow = _sendDeviceConfigFlow.asSharedFlow() - - private suspend fun sendDeviceConfig(config: ConfigProtos.Config) { - debug("Sending device config!") - _sendDeviceConfigFlow.emit(config) - } + private val _setConfigFlow = MutableSharedFlow() + val setConfigFlow: SharedFlow = _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() } diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigRepository.kt new file mode 100644 index 000000000..de292d22b --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigRepository.kt @@ -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, +) : Logging { + val moduleConfigFlow: Flow = 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() + +} diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigSerializer.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigSerializer.kt new file mode 100644 index 000000000..6aa516ab6 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigSerializer.kt @@ -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 { + 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) +} diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 6cdf9b6ad..653fc0f85 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -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) diff --git a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt similarity index 74% rename from app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt rename to app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt index a936190ca..17750d258 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt @@ -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) } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/PreferenceItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsItemList.kt similarity index 99% rename from app/src/main/java/com/geeksville/mesh/ui/PreferenceItemList.kt rename to app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsItemList.kt index e70f642e2..cb371d1fa 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/PreferenceItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsItemList.kt @@ -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() diff --git a/app/src/main/java/com/geeksville/mesh/ui/ModuleSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ModuleSettingsFragment.kt new file mode 100644 index 000000000..f06a74f49 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/ModuleSettingsFragment.kt @@ -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 + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/ModuleSettingsItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/ModuleSettingsItemList.kt new file mode 100644 index 000000000..eaeb9622b --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/ModuleSettingsItemList.kt @@ -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 } + }) + } + } +} + diff --git a/app/src/main/java/com/geeksville/mesh/ui/PreferenceScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/PreferenceScreen.kt deleted file mode 100644 index 750113f9c..000000000 --- a/app/src/main/java/com/geeksville/mesh/ui/PreferenceScreen.kt +++ /dev/null @@ -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 { })) -// } -//} diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 12e7c2ef0..c6e119162 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -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) diff --git a/app/src/main/res/layout/advanced_settings.xml b/app/src/main/res/layout/compose_view.xml similarity index 85% rename from app/src/main/res/layout/advanced_settings.xml rename to app/src/main/res/layout/compose_view.xml index 94ff4e872..645489b89 100644 --- a/app/src/main/res/layout/advanced_settings.xml +++ b/app/src/main/res/layout/compose_view.xml @@ -7,7 +7,7 @@ android:background="@color/colorAdvancedBackground"> diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index d7817540f..69a607792 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -17,9 +17,13 @@ android:checked="false" android:title="@string/protocol_stress_test" /> + android:title="@string/device_settings" /> + Período de reposo del dispositivo (en segundos) Notificaciones de mensajes Protocolo de prueba de esfuerzo - Configuración avanzada Es necesario actualizar el firmware Vale ¡Debe establecer una región! diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index b08aeae42..6e8f1beab 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -76,7 +76,6 @@ Eszköz alvásának gyakorisága (másodpercben) Értesítések az üzenetekről Protokoll stressz teszt - Haladó beállítások Firmware frissítés szükséges 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 firmware frissítés leírásában talál, a Github-on. OK diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index fdbceb527..8e23145bb 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -82,7 +82,6 @@ 기기가 절전모드 들어가기까지의 시간 (in seconds) 메시지 알림 프로토콜 스트레스 테스트 - 고급 설정 펌웨어 업데이트 필요 이 기기의 펌웨어가 매우 오래되어 이 앱과 호환되지않습니다, 설정에서 "펌웨어 업데이트"를 선택하여주세요. 더 자세한정보는 깃허브의 에서 펌웨어 업데이트 가이드를 참고해주세요.. 확인 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 63bf9d8d7..57896332d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -77,7 +77,6 @@ Okres uśpienia urządzenia (w sekundach) Powiadomienia o wiadomościach Protokół testu warunków skrajnych - Zaawansowane ustawienia Wymagana aktualizacja oprogramowania układowego 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 nasz przewodnik instalacji oprogramowania układowego na Github. OK diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2d359f9ee..7ac9000b4 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -77,7 +77,6 @@ Intervalo de suspensão (sleep) (em segundos) Notificações sobre mensagens Stress test do protocolo - Configurações avançadas Atualização do firmware necessária 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 Nosso guia de instalação de firmware no Github. Okay diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 2408b7bc2..a06ccaffd 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -77,7 +77,6 @@ Intervalo de suspensão (sleep) (segundos) Notificações sobre mensagens Stress test do protocolo - Configurações avançadas Atualização do firmware necessária 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 Nosso guia de instalação de firmware no Github. Okay diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 3bc2e553f..210166121 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -77,7 +77,6 @@ Interval uspávania zariadenia (v sekundách) Upozornenia na správy Stres test protokolu - Rozšírené nastavenia Nutná aktualizácia firmvéru vysielača Ok Musíte nastaviť región! diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index d6bf80663..c0415f39c 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -77,7 +77,6 @@ 设备休眠时间(以秒为单位) 关于消息的通知 协议压力测试 - 高级设置 需要固件更新 固件太旧,无法与此应用程序对话,请转到设置窗头并选择"升级固件". 有关这方面的更多信息,请参阅固件安装指南 在GitHub 好的 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c31993196..593e743e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,7 +81,6 @@ Device sleep period (in seconds) Notifications about messages Protocol stress test - Advanced settings Firmware update required 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 our Firmware Installation guide on Github. Okay @@ -163,4 +162,6 @@ Start Download Request position Close + Device settings + Module settings