From 7869243290dc0542fa65c9337674606033fa2c24 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 16 Sep 2023 09:51:16 -0300 Subject: [PATCH] refactor(config): move business logic to ViewModel --- .../mesh/database/entity/MeshLog.kt | 10 - .../java/com/geeksville/mesh/model/Channel.kt | 4 +- .../java/com/geeksville/mesh/model/UIState.kt | 212 +++++++++- .../com/geeksville/mesh/ui/ChannelFragment.kt | 2 +- .../mesh/ui/DeviceSettingsFragment.kt | 390 ++++++------------ .../geeksville/mesh/ui/SettingsFragment.kt | 2 +- .../com/geeksville/mesh/ui/UsersFragment.kt | 24 +- .../config/PacketResponseStateDialog.kt | 33 +- 8 files changed, 354 insertions(+), 323 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/MeshLog.kt b/app/src/main/java/com/geeksville/mesh/database/entity/MeshLog.kt index b0bc0570c..a1a1bd448 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/MeshLog.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/MeshLog.kt @@ -51,14 +51,4 @@ data class MeshLog(@PrimaryKey val uuid: String, return null } ?: nodeInfo?.position } - - val routeDiscovery: MeshProtos.RouteDiscovery? - get() { - return meshPacket?.run { - if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.TRACEROUTE_APP_VALUE) { - return MeshProtos.RouteDiscovery.parseFrom(decoded.payload) - } - return null - } - } } diff --git a/app/src/main/java/com/geeksville/mesh/model/Channel.kt b/app/src/main/java/com/geeksville/mesh/model/Channel.kt index cfac363c8..a41d70cfb 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt @@ -12,8 +12,8 @@ fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos]. fun xorHash(b: ByteArray) = b.fold(0) { acc, x -> acc xor (x.toInt() and 0xff) } data class Channel( - val settings: ChannelProtos.ChannelSettings, - val loraConfig: ConfigProtos.Config.LoRaConfig + val settings: ChannelProtos.ChannelSettings = default.settings, + val loraConfig: ConfigProtos.Config.LoRaConfig = default.loraConfig, ) { companion object { // These bytes must match the well known and not secret bytes used the default channel AES128 key device code 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 15c444d4d..b4c473c3a 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -30,6 +30,8 @@ import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.geeksville.mesh.service.MeshService +import com.geeksville.mesh.ui.ConfigRoute +import com.geeksville.mesh.ui.ResponseState import com.geeksville.mesh.util.positionToMeter import com.google.protobuf.MessageLite import dagger.hilt.android.lifecycle.HiltViewModel @@ -43,6 +45,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.BufferedWriter @@ -77,6 +80,20 @@ fun getInitials(nameIn: String): String { return initials.take(nchars) } +/** + * Data class that represents the current RadioConfig state. + */ +data class RadioConfigState( + val route: String = "", + val userConfig: User = User.getDefaultInstance(), + val channelList: List = emptyList(), + val radioConfig: Config = Config.getDefaultInstance(), + val moduleConfig: ModuleConfig = ModuleConfig.getDefaultInstance(), + val ringtone: String = "", + val cannedMessageMessages: String = "", + val responseState: ResponseState = ResponseState.Empty, +) + @HiltViewModel class UIViewModel @Inject constructor( private val app: Application, @@ -119,8 +136,8 @@ class UIViewModel @Inject constructor( val ourNodeInfo: StateFlow = _ourNodeInfo private val requestId = MutableStateFlow(null) - private val _packetResponse = MutableStateFlow(null) - val packetResponse: StateFlow = _packetResponse + private val _radioConfigState = MutableStateFlow(RadioConfigState()) + val radioConfigState: StateFlow = _radioConfigState init { radioConfigRepository.nodeInfoFlow().onEach(nodeDB::setNodes) @@ -154,7 +171,7 @@ class UIViewModel @Inject constructor( viewModelScope.launch { combine(meshLogRepository.getAllLogs(9), requestId) { list, id -> list.takeIf { id != null }?.firstOrNull { it.meshPacket?.decoded?.requestId == id } - }.collect { response -> _packetResponse.value = response } + }.collect(::processPacketResponse) } debug("ViewModel created") @@ -194,14 +211,6 @@ class UIViewModel @Inject constructor( _destNode.value = node } - /** - * Called immediately after activity observes packetResponse - */ - fun clearPacketResponse() { - requestId.value = null - _packetResponse.value = null - } - fun generatePacketId(): Int? { return try { meshService?.packetId @@ -278,6 +287,7 @@ class UIViewModel @Inject constructor( ) fun setRingtone(destNum: Int, ringtone: String) { + _radioConfigState.update { it.copy(ringtone = ringtone) } meshService?.setRingtone(destNum, ringtone) } @@ -288,6 +298,7 @@ class UIViewModel @Inject constructor( ) fun setCannedMessages(destNum: Int, messages: String) { + _radioConfigState.update { it.copy(cannedMessageMessages = messages) } meshService?.setCannedMessages(destNum, messages) } @@ -422,6 +433,7 @@ class UIViewModel @Inject constructor( private val _myNodeInfo = MutableLiveData() val myNodeInfo: LiveData get() = _myNodeInfo val myNodeNum get() = _myNodeInfo.value?.myNodeNum + val maxChannels = myNodeInfo.value?.maxChannels ?: 8 fun setMyNodeInfo(info: MyNodeInfo?) { _myNodeInfo.value = info @@ -461,10 +473,12 @@ class UIViewModel @Inject constructor( } fun setRemoteConfig(destNum: Int, config: Config) { + _radioConfigState.update { it.copy(radioConfig = config) } meshService?.setRemoteConfig(destNum, config.toByteArray()) } fun setModuleConfig(destNum: Int, config: ModuleConfig) { + _radioConfigState.update { it.copy(moduleConfig = config) } meshService?.setModuleConfig(destNum, config.toByteArray()) } @@ -554,6 +568,7 @@ class UIViewModel @Inject constructor( try { // Note: we use ?. here because we might be running in the emulator meshService?.setRemoteOwner(destNum, user.toByteArray()) + _radioConfigState.update { it.copy(userConfig = user) } } catch (ex: RemoteException) { errormsg("Can't set username on device, is device offline? ${ex.message}") } @@ -566,7 +581,7 @@ class UIViewModel @Inject constructor( /** * Write the persisted packet data out to a CSV file in the specified location. */ - fun saveMessagesCSV(file_uri: Uri) { + fun saveMessagesCSV(uri: Uri) { viewModelScope.launch(Dispatchers.Main) { // Extract distances to this device from position messages and put (node,SNR,distance) in // the file_uri @@ -581,7 +596,7 @@ class UIViewModel @Inject constructor( } } - writeToUri(file_uri) { writer -> + writeToUri(uri) { writer -> // Create a map of nodes keyed by their ID val nodesById = nodes.values.associateBy { it.num }.toMutableMap() val nodePositions = mutableMapOf() @@ -681,19 +696,23 @@ class UIViewModel @Inject constructor( _deviceProfile.value = deviceProfile } - fun importProfile(file_uri: Uri) = viewModelScope.launch(Dispatchers.Main) { - withContext(Dispatchers.IO) { - app.contentResolver.openInputStream(file_uri).use { inputStream -> + fun importProfile(uri: Uri) = viewModelScope.launch(Dispatchers.IO) { + try { + app.contentResolver.openInputStream(uri).use { inputStream -> val bytes = inputStream?.readBytes() val protobuf = DeviceProfile.parseFrom(bytes) _deviceProfile.value = protobuf } + } catch (ex: Exception) { + val error = "${ex.javaClass.simpleName}: ${ex.message}" + errormsg("Import DeviceProfile error: ${ex.message}") + setResponseStateError(error) } } - fun exportProfile(file_uri: Uri) = viewModelScope.launch { + fun exportProfile(uri: Uri) = viewModelScope.launch { val profile = deviceProfile.value ?: return@launch - writeToUri(file_uri, profile) + writeToUri(uri, profile) _deviceProfile.value = null } @@ -783,5 +802,160 @@ class UIViewModel @Inject constructor( } } } -} + fun clearPacketResponse() { + _radioConfigState.update { it.copy(responseState = ResponseState.Empty) } + } + + fun setResponseStateLoading(route: String) { + _radioConfigState.value = RadioConfigState( + route = route, + responseState = ResponseState.Loading(total = 1), + ) + } + + fun setResponseStateTotal(total: Int) { + _radioConfigState.update { state -> + if (state.responseState is ResponseState.Loading) { + state.copy(responseState = state.responseState.copy(total = total)) + } else { + state // Return the unchanged state for other response states + } + } + } + + private fun setResponseStateError(error: String) { + _radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) } + } + + private fun incrementCompleted() { + _radioConfigState.update { state -> + if (state.responseState is ResponseState.Loading) { + val increment = state.responseState.completed + 1 + state.copy(responseState = state.responseState.copy(completed = increment)) + } else { + state // Return the unchanged state for other response states + } + } + } + + fun clearRemoteChannelList() { + _radioConfigState.update { it.copy(channelList = emptyList()) } + } + + fun setRemoteChannelList(list: List) { + _radioConfigState.update { it.copy(channelList = list) } + } + + private val _tracerouteResponse = MutableLiveData(null) + val tracerouteResponse: LiveData get() = _tracerouteResponse + + fun clearTracerouteResponse() { + _tracerouteResponse.value = null + } + + private fun processPacketResponse(log: MeshLog?) { + val destNum = destNode.value?.num ?: return + val packet = log?.meshPacket ?: return + val data = packet.decoded + val destStr = destNum.toUInt() + val fromStr = packet.from.toUInt() + requestId.value = null + + if (data?.portnumValue == Portnums.PortNum.TRACEROUTE_APP_VALUE) { + val parsed = MeshProtos.RouteDiscovery.parseFrom(data.payload) + fun nodeName(num: Int) = nodeDB.nodesByNum?.get(num)?.user?.longName + ?: app.getString(R.string.unknown_username) + + _tracerouteResponse.value = buildString { + append("${nodeName(packet.to)} --> ") + parsed.routeList.forEach { num -> append("${nodeName(num)} --> ") } + append(nodeName(packet.from)) + } + } + if (data?.portnumValue == Portnums.PortNum.ROUTING_APP_VALUE) { + val parsed = MeshProtos.Routing.parseFrom(data.payload) + debug("packet for destNum $destStr received ${parsed.errorReason} from $fromStr") + if (parsed.errorReason != MeshProtos.Routing.Error.NONE) { + setResponseStateError(parsed.errorReason.toString()) + } else if (packet.from == destNum) { + _radioConfigState.update { it.copy(responseState = ResponseState.Success(true)) } + } + } + if (data?.portnumValue == Portnums.PortNum.ADMIN_APP_VALUE) { + val parsed = AdminProtos.AdminMessage.parseFrom(data.payload) + debug("packet for destNum $destStr received ${parsed.payloadVariantCase} from $fromStr") + if (destNum != packet.from) { + setResponseStateError("Unexpected sender: $fromStr instead of $destStr.") + return + } + // check destination: lora config or channel editor + val goChannels = radioConfigState.value.route == ConfigRoute.CHANNELS.name + when (parsed.payloadVariantCase) { + AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> { + val response = parsed.getChannelResponse + incrementCompleted() + // Stop once we get to the first disabled entry + if (response.role != ChannelProtos.Channel.Role.DISABLED) { + _radioConfigState.update { state -> + val updatedList = state.channelList.toMutableList().apply { + add(response.index, response.settings) + } + state.copy(channelList = updatedList) + } + if (response.index + 1 < maxChannels && goChannels) { + // Not done yet, request next channel + getChannel(destNum, response.index + 1) + } else { + // Received max channels, get lora config (for default channel names) + getConfig(destNum, AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE) + } + } else { + // Received last channel, get lora config (for default channel names) + setResponseStateTotal(radioConfigState.value.channelList.size + 1) + getConfig(destNum, AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE) + } + } + + AdminProtos.AdminMessage.PayloadVariantCase.GET_OWNER_RESPONSE -> { + _radioConfigState.update { it.copy(userConfig = parsed.getOwnerResponse) } + incrementCompleted() + } + + AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> { + val response = parsed.getConfigResponse + if (response.payloadVariantCase.number == 0) { // PAYLOADVARIANT_NOT_SET + setResponseStateError(response.payloadVariantCase.name) + } + _radioConfigState.update { it.copy(radioConfig = response) } + incrementCompleted() + } + + AdminProtos.AdminMessage.PayloadVariantCase.GET_MODULE_CONFIG_RESPONSE -> { + val response = parsed.getModuleConfigResponse + if (response.payloadVariantCase.number == 0) { // PAYLOADVARIANT_NOT_SET + setResponseStateError(response.payloadVariantCase.name) + } + _radioConfigState.update { it.copy(moduleConfig = response) } + incrementCompleted() + } + + AdminProtos.AdminMessage.PayloadVariantCase.GET_CANNED_MESSAGE_MODULE_MESSAGES_RESPONSE -> { + _radioConfigState.update { + it.copy(cannedMessageMessages = parsed.getCannedMessageModuleMessagesResponse) + } + incrementCompleted() + getModuleConfig(destNum, AdminProtos.AdminMessage.ModuleConfigType.CANNEDMSG_CONFIG_VALUE) + } + + AdminProtos.AdminMessage.PayloadVariantCase.GET_RINGTONE_RESPONSE -> { + _radioConfigState.update { it.copy(ringtone = parsed.getRingtoneResponse) } + incrementCompleted() + getModuleConfig(destNum, AdminProtos.AdminMessage.ModuleConfigType.EXTNOTIF_CONFIG_VALUE) + } + + else -> TODO() + } + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt index 7db520873..37282377b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -143,7 +143,7 @@ fun ChannelScreen( val primaryChannel = ChannelSet(channelSet).primaryChannel val channelUrl = ChannelSet(channelSet).getChannelUrl() - val modemPresetName = Channel(Channel.default.settings, channelSet.loraConfig).name + val modemPresetName = Channel(loraConfig = channelSet.loraConfig).name val barcodeLauncher = rememberLauncherForActivityResult(ScanContract()) { result -> if (result.contents != null) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt index f81cbc870..8434d9e66 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt @@ -36,10 +36,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.twotone.KeyboardArrowRight import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -59,17 +57,8 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.geeksville.mesh.AdminProtos -import com.geeksville.mesh.AdminProtos.AdminMessage.ConfigType -import com.geeksville.mesh.AdminProtos.AdminMessage.ModuleConfigType -import com.geeksville.mesh.ChannelProtos -import com.geeksville.mesh.ConfigProtos.Config -import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig -import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.NodeInfo -import com.geeksville.mesh.Portnums import com.geeksville.mesh.R -import com.geeksville.mesh.android.BuildUtils.debug import com.geeksville.mesh.android.Logging import com.geeksville.mesh.config import com.geeksville.mesh.deviceProfile @@ -126,7 +115,7 @@ class DeviceSettingsFragment : ScreenFragment("Radio Configuration"), Logging { // val backStackEntry by navController.currentBackStackEntryAsState() // Get the name of the current screen // val currentScreen = backStackEntry?.destination?.route?.let { route -> - // enumValues().find { it.name == route }?.title ?: "home" + // NavRoute.entries.find { it.name == route }?.title // } Scaffold( @@ -160,45 +149,51 @@ class DeviceSettingsFragment : ScreenFragment("Radio Configuration"), Logging { } } -enum class ConfigDest(val title: String) { - DEVICE("Device"), - POSITION("Position"), - POWER("Power"), - NETWORK("Network"), - DISPLAY("Display"), - LORA("LoRa"), - BLUETOOTH("Bluetooth"), +// Config (configType = AdminProtos.AdminMessage.ConfigType) +enum class ConfigRoute(val title: String, val configType: Int = 0) { + USER("User"), + CHANNELS("Channels"), + DEVICE("Device", 0), + POSITION("Position", 1), + POWER("Power", 2), + NETWORK("Network", 3), + DISPLAY("Display", 4), + LORA("LoRa", 5), + BLUETOOTH("Bluetooth", 6), ; } -enum class ModuleDest(val title: String) { - MQTT("MQTT"), - SERIAL("Serial"), - EXTERNAL_NOTIFICATION("External Notification"), - STORE_FORWARD("Store & Forward"), - RANGE_TEST("Range Test"), - TELEMETRY("Telemetry"), - CANNED_MESSAGE("Canned Message"), - AUDIO("Audio"), - REMOTE_HARDWARE("Remote Hardware"), - NEIGHBOR_INFO("Neighbor Info"), - AMBIENT_LIGHTING("Ambient Lighting"), - DETECTION_SENSOR("Detection Sensor"), +// ModuleConfig (configType = AdminProtos.AdminMessage.ModuleConfigType) +enum class ModuleRoute(val title: String, val configType: Int = 0) { + MQTT("MQTT", 0), + SERIAL("Serial", 1), + EXTERNAL_NOTIFICATION("External Notification", 2), + STORE_FORWARD("Store & Forward", 3), + RANGE_TEST("Range Test", 4), + TELEMETRY("Telemetry", 5), + CANNED_MESSAGE("Canned Message", 6), + AUDIO("Audio", 7), + REMOTE_HARDWARE("Remote Hardware", 8), + NEIGHBOR_INFO("Neighbor Info", 9), + AMBIENT_LIGHTING("Ambient Lighting", 10), + DETECTION_SENSOR("Detection Sensor", 11), ; } +private fun getName(route: Any): String = when (route) { + is ConfigRoute -> route.name + is ModuleRoute -> route.name + else -> "" +} + /** - * This sealed class defines each possible state of a packet response. + * Generic sealed class defines each possible state of a response. */ -sealed class PacketResponseState { - object Loading : PacketResponseState() { - var total: Int = 0 - var completed: Int = 0 - } - - data class Success(val packets: List) : PacketResponseState() - object Empty : PacketResponseState() - data class Error(val error: String) : PacketResponseState() +sealed class ResponseState { + data object Empty : ResponseState() + data class Loading(var total: Int = 0, var completed: Int = 0) : ResponseState() + data class Success(val result: T) : ResponseState() + data class Error(val error: String) : ResponseState() } @Composable @@ -236,21 +231,13 @@ fun RadioConfigNavHost( val destNum = node.num val isLocal = destNum == viewModel.myNodeNum - val maxChannels = viewModel.myNodeInfo.value?.maxChannels ?: 8 + val maxChannels = viewModel.maxChannels - var userConfig by remember { mutableStateOf(MeshProtos.User.getDefaultInstance()) } - val channelList = remember { mutableStateListOf() } - var radioConfig by remember { mutableStateOf(Config.getDefaultInstance()) } - var moduleConfig by remember { mutableStateOf(ModuleConfig.getDefaultInstance()) } + val radioConfigState by viewModel.radioConfigState.collectAsStateWithLifecycle() + var location by remember(node) { mutableStateOf(node.position) } // FIXME - var location by remember(node) { mutableStateOf(node.position) } - var ringtone by remember { mutableStateOf("") } - var cannedMessageMessages by remember { mutableStateOf("") } - - val packetResponse by viewModel.packetResponse.collectAsStateWithLifecycle() val deviceProfile by viewModel.deviceProfile.collectAsStateWithLifecycle() - var packetResponseState by remember { mutableStateOf(PacketResponseState.Empty) } - val isWaiting = packetResponseState !is PacketResponseState.Empty + val isWaiting = radioConfigState.responseState !is ResponseState.Empty var showEditDeviceProfileDialog by remember { mutableStateOf(false) } val importConfigLauncher = rememberLauncherForActivityResult( @@ -258,7 +245,7 @@ fun RadioConfigNavHost( ) { if (it.resultCode == Activity.RESULT_OK) { showEditDeviceProfileDialog = true - it.data?.data?.let { file_uri -> viewModel.importProfile(file_uri) } + it.data?.data?.let { uri -> viewModel.importProfile(uri) } } } @@ -266,7 +253,7 @@ fun RadioConfigNavHost( ActivityResultContracts.StartActivityForResult() ) { if (it.resultCode == Activity.RESULT_OK) { - it.data?.data?.let { file_uri -> viewModel.exportProfile(file_uri) } + it.data?.data?.let { uri -> viewModel.exportProfile(uri) } } } @@ -304,91 +291,17 @@ fun RadioConfigNavHost( ) if (isWaiting) PacketResponseStateDialog( - packetResponseState, + radioConfigState.responseState, onDismiss = { - packetResponseState = PacketResponseState.Empty + showEditDeviceProfileDialog = false + viewModel.clearPacketResponse() + }, + onComplete = { + navController.navigate(radioConfigState.route) viewModel.clearPacketResponse() } ) - if (isWaiting) LaunchedEffect(packetResponse) { - val data = packetResponse?.meshPacket?.decoded - val from = packetResponse?.meshPacket?.from?.toUInt() - if (data?.portnumValue == Portnums.PortNum.ROUTING_APP_VALUE) { - val parsed = MeshProtos.Routing.parseFrom(data.payload) - debug("packet for destNum ${destNum.toUInt()} received ${parsed.errorReason} from $from") - if (parsed.errorReason != MeshProtos.Routing.Error.NONE) { - packetResponseState = PacketResponseState.Error(parsed.errorReason.toString()) - } else if (packetResponse?.meshPacket?.from == destNum) { - packetResponseState = PacketResponseState.Success(emptyList()) - } - } - if (data?.portnumValue == Portnums.PortNum.ADMIN_APP_VALUE) { - viewModel.clearPacketResponse() - val parsed = AdminProtos.AdminMessage.parseFrom(data.payload) - debug("packet for destNum ${destNum.toUInt()} received ${parsed.payloadVariantCase} from $from") - // check destination: lora config or channel editor - val goChannels = (packetResponseState as PacketResponseState.Loading).total > 2 - when (parsed.payloadVariantCase) { - AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> { - val response = parsed.getChannelResponse - (packetResponseState as PacketResponseState.Loading).completed++ - // Stop once we get to the first disabled entry - if (response.role != ChannelProtos.Channel.Role.DISABLED) { - channelList.add(response.index, response.settings) - if (response.index + 1 < maxChannels && goChannels) { - // Not done yet, request next channel - viewModel.getChannel(destNum, response.index + 1) - } else { - // Received max channels, get lora config (for default channel names) - viewModel.getConfig(destNum, ConfigType.LORA_CONFIG_VALUE) - } - } else { - // Received last channel, get lora config (for default channel names) - viewModel.getConfig(destNum, ConfigType.LORA_CONFIG_VALUE) - } - } - - AdminProtos.AdminMessage.PayloadVariantCase.GET_OWNER_RESPONSE -> { - packetResponseState = PacketResponseState.Empty - userConfig = parsed.getOwnerResponse - navController.navigate("user") - } - - AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> { - packetResponseState = PacketResponseState.Empty - val response = parsed.getConfigResponse - radioConfig = response - if (goChannels) navController.navigate("channels") - else enumValues().find { it.name == "${response.payloadVariantCase}" } - ?.let { navController.navigate(it.name) } - } - - AdminProtos.AdminMessage.PayloadVariantCase.GET_MODULE_CONFIG_RESPONSE -> { - packetResponseState = PacketResponseState.Empty - val response = parsed.getModuleConfigResponse - moduleConfig = response - enumValues().find { it.name == "${response.payloadVariantCase}" } - ?.let { navController.navigate(it.name) } - } - - AdminProtos.AdminMessage.PayloadVariantCase.GET_CANNED_MESSAGE_MODULE_MESSAGES_RESPONSE -> { - cannedMessageMessages = parsed.getCannedMessageModuleMessagesResponse - (packetResponseState as PacketResponseState.Loading).completed++ - viewModel.getModuleConfig(destNum, ModuleConfigType.CANNEDMSG_CONFIG_VALUE) - } - - AdminProtos.AdminMessage.PayloadVariantCase.GET_RINGTONE_RESPONSE -> { - ringtone = parsed.getRingtoneResponse - (packetResponseState as PacketResponseState.Loading).completed++ - viewModel.getModuleConfig(destNum, ModuleConfigType.EXTNOTIF_CONFIG_VALUE) - } - - else -> TODO() - } - } - } - NavHost( navController = navController, startDestination = "home", @@ -398,22 +311,17 @@ fun RadioConfigNavHost( RadioSettingsScreen( enabled = connected && !isWaiting, isLocal = isLocal, - onRouteClick = { configType -> - packetResponseState = PacketResponseState.Loading.apply { - total = 1 - completed = 0 - } - // clearAllConfigs() ? - when (configType) { - "USER" -> { viewModel.getOwner(destNum) } - "CHANNELS" -> { - val maxPackets = maxChannels + 1 // for lora config - (packetResponseState as PacketResponseState.Loading).total = maxPackets - channelList.clear() + onRouteClick = { route -> + viewModel.setResponseStateLoading(getName(route)) + when (route) { + ConfigRoute.USER -> { viewModel.getOwner(destNum) } + ConfigRoute.CHANNELS -> { + viewModel.setResponseStateTotal(maxChannels + 1) // for lora config + viewModel.clearRemoteChannelList() viewModel.getChannel(destNum, 0) } "IMPORT" -> { - packetResponseState = PacketResponseState.Empty + viewModel.clearPacketResponse() viewModel.setDeviceProfile(null) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) @@ -422,7 +330,7 @@ fun RadioConfigNavHost( importConfigLauncher.launch(intent) } "EXPORT" -> { - packetResponseState = PacketResponseState.Empty + viewModel.clearPacketResponse() showEditDeviceProfileDialog = true } @@ -442,281 +350,258 @@ fun RadioConfigNavHost( viewModel.requestNodedbReset(destNum) } - ConfigDest.LORA -> { - (packetResponseState as PacketResponseState.Loading).total = 2 - channelList.clear() + ConfigRoute.LORA -> { + viewModel.setResponseStateTotal(2) + viewModel.clearRemoteChannelList() viewModel.getChannel(destNum, 0) } - is ConfigDest -> { - viewModel.getConfig(destNum, configType.ordinal) + is ConfigRoute -> { + viewModel.getConfig(destNum, route.configType) } - ModuleDest.CANNED_MESSAGE -> { - (packetResponseState as PacketResponseState.Loading).total = 2 + ModuleRoute.CANNED_MESSAGE -> { + viewModel.setResponseStateTotal(2) viewModel.getCannedMessages(destNum) } - ModuleDest.EXTERNAL_NOTIFICATION -> { - (packetResponseState as PacketResponseState.Loading).total = 2 + ModuleRoute.EXTERNAL_NOTIFICATION -> { + viewModel.setResponseStateTotal(2) viewModel.getRingtone(destNum) } - is ModuleDest -> { - viewModel.getModuleConfig(destNum, configType.ordinal) + is ModuleRoute -> { + viewModel.getModuleConfig(destNum, route.configType) } } }, ) } - composable("channels") { - ChannelSettingsItemList( - settingsList = channelList, - modemPresetName = Channel(Channel.default.settings, radioConfig.lora).name, - enabled = connected, - maxChannels = maxChannels, - onPositiveClicked = { channelListInput -> - viewModel.updateChannels(destNum, channelList, channelListInput) - channelList.clear() - channelList.addAll(channelListInput) - }, - ) - } - composable("user") { + composable(ConfigRoute.USER.name) { UserConfigItemList( - userConfig = userConfig, + userConfig = radioConfigState.userConfig, enabled = connected, onSaveClicked = { userInput -> viewModel.setRemoteOwner(destNum, userInput) - userConfig = userInput } ) } - composable(ConfigDest.DEVICE.name) { + composable(ConfigRoute.CHANNELS.name) { + ChannelSettingsItemList( + settingsList = radioConfigState.channelList, + modemPresetName = Channel(loraConfig = radioConfigState.radioConfig.lora).name, + enabled = connected, + maxChannels = maxChannels, + onPositiveClicked = { channelListInput -> + viewModel.updateChannels(destNum, radioConfigState.channelList, channelListInput) + viewModel.setRemoteChannelList(channelListInput) + }, + ) + } + composable(ConfigRoute.DEVICE.name) { DeviceConfigItemList( - deviceConfig = radioConfig.device, + deviceConfig = radioConfigState.radioConfig.device, enabled = connected, onSaveClicked = { deviceInput -> val config = config { device = deviceInput } viewModel.setRemoteConfig(destNum, config) - radioConfig = config } ) } - composable(ConfigDest.POSITION.name) { + composable(ConfigRoute.POSITION.name) { PositionConfigItemList( isLocal = isLocal, location = location, - positionConfig = radioConfig.position, + positionConfig = radioConfigState.radioConfig.position, enabled = connected, onSaveClicked = { locationInput, positionInput -> if (locationInput != node.position && positionInput.fixedPosition) { locationInput?.let { viewModel.requestPosition(destNum, it) } location = locationInput } - if (positionInput != radioConfig.position) { + if (positionInput != radioConfigState.radioConfig.position) { val config = config { position = positionInput } viewModel.setRemoteConfig(destNum, config) - radioConfig = config } } ) } - composable(ConfigDest.POWER.name) { + composable(ConfigRoute.POWER.name) { PowerConfigItemList( - powerConfig = radioConfig.power, + powerConfig = radioConfigState.radioConfig.power, enabled = connected, onSaveClicked = { powerInput -> val config = config { power = powerInput } viewModel.setRemoteConfig(destNum, config) - radioConfig = config } ) } - composable(ConfigDest.NETWORK.name) { + composable(ConfigRoute.NETWORK.name) { NetworkConfigItemList( - networkConfig = radioConfig.network, + networkConfig = radioConfigState.radioConfig.network, enabled = connected, onSaveClicked = { networkInput -> val config = config { network = networkInput } viewModel.setRemoteConfig(destNum, config) - radioConfig = config } ) } - composable(ConfigDest.DISPLAY.name) { + composable(ConfigRoute.DISPLAY.name) { DisplayConfigItemList( - displayConfig = radioConfig.display, + displayConfig = radioConfigState.radioConfig.display, enabled = connected, onSaveClicked = { displayInput -> val config = config { display = displayInput } viewModel.setRemoteConfig(destNum, config) - radioConfig = config } ) } - composable(ConfigDest.LORA.name) { + composable(ConfigRoute.LORA.name) { LoRaConfigItemList( - loraConfig = radioConfig.lora, - primarySettings = channelList.getOrNull(0) ?: return@composable, + loraConfig = radioConfigState.radioConfig.lora, + primarySettings = radioConfigState.channelList.getOrNull(0) ?: return@composable, enabled = connected, onSaveClicked = { loraInput -> val config = config { lora = loraInput } viewModel.setRemoteConfig(destNum, config) - radioConfig = config } ) } - composable(ConfigDest.BLUETOOTH.name) { + composable(ConfigRoute.BLUETOOTH.name) { BluetoothConfigItemList( - bluetoothConfig = radioConfig.bluetooth, + bluetoothConfig = radioConfigState.radioConfig.bluetooth, enabled = connected, onSaveClicked = { bluetoothInput -> val config = config { bluetooth = bluetoothInput } viewModel.setRemoteConfig(destNum, config) - radioConfig = config } ) } - composable(ModuleDest.MQTT.name) { + composable(ModuleRoute.MQTT.name) { MQTTConfigItemList( - mqttConfig = moduleConfig.mqtt, + mqttConfig = radioConfigState.moduleConfig.mqtt, enabled = connected, onSaveClicked = { mqttInput -> val config = moduleConfig { mqtt = mqttInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } ) } - composable(ModuleDest.SERIAL.name) { + composable(ModuleRoute.SERIAL.name) { SerialConfigItemList( - serialConfig = moduleConfig.serial, + serialConfig = radioConfigState.moduleConfig.serial, enabled = connected, onSaveClicked = { serialInput -> val config = moduleConfig { serial = serialInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } ) } - composable(ModuleDest.EXTERNAL_NOTIFICATION.name) { + composable(ModuleRoute.EXTERNAL_NOTIFICATION.name) { ExternalNotificationConfigItemList( - ringtone = ringtone, - extNotificationConfig = moduleConfig.externalNotification, + ringtone = radioConfigState.ringtone, + extNotificationConfig = radioConfigState.moduleConfig.externalNotification, enabled = connected, onSaveClicked = { ringtoneInput, extNotificationInput -> - if (ringtoneInput != ringtone) { + if (ringtoneInput != radioConfigState.ringtone) { viewModel.setRingtone(destNum, ringtoneInput) - ringtone = ringtoneInput } - if (extNotificationInput != moduleConfig.externalNotification) { + if (extNotificationInput != radioConfigState.moduleConfig.externalNotification) { val config = moduleConfig { externalNotification = extNotificationInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } } ) } - composable(ModuleDest.STORE_FORWARD.name) { + composable(ModuleRoute.STORE_FORWARD.name) { StoreForwardConfigItemList( - storeForwardConfig = moduleConfig.storeForward, + storeForwardConfig = radioConfigState.moduleConfig.storeForward, enabled = connected, onSaveClicked = { storeForwardInput -> val config = moduleConfig { storeForward = storeForwardInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } ) } - composable(ModuleDest.RANGE_TEST.name) { + composable(ModuleRoute.RANGE_TEST.name) { RangeTestConfigItemList( - rangeTestConfig = moduleConfig.rangeTest, + rangeTestConfig = radioConfigState.moduleConfig.rangeTest, enabled = connected, onSaveClicked = { rangeTestInput -> val config = moduleConfig { rangeTest = rangeTestInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } ) } - composable(ModuleDest.TELEMETRY.name) { + composable(ModuleRoute.TELEMETRY.name) { TelemetryConfigItemList( - telemetryConfig = moduleConfig.telemetry, + telemetryConfig = radioConfigState.moduleConfig.telemetry, enabled = connected, onSaveClicked = { telemetryInput -> val config = moduleConfig { telemetry = telemetryInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } ) } - composable(ModuleDest.CANNED_MESSAGE.name) { + composable(ModuleRoute.CANNED_MESSAGE.name) { CannedMessageConfigItemList( - messages = cannedMessageMessages, - cannedMessageConfig = moduleConfig.cannedMessage, + messages = radioConfigState.cannedMessageMessages, + cannedMessageConfig = radioConfigState.moduleConfig.cannedMessage, enabled = connected, onSaveClicked = { messagesInput, cannedMessageInput -> - if (messagesInput != cannedMessageMessages) { + if (messagesInput != radioConfigState.cannedMessageMessages) { viewModel.setCannedMessages(destNum, messagesInput) - cannedMessageMessages = messagesInput } - if (cannedMessageInput != moduleConfig.cannedMessage) { + if (cannedMessageInput != radioConfigState.moduleConfig.cannedMessage) { val config = moduleConfig { cannedMessage = cannedMessageInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } } ) } - composable(ModuleDest.AUDIO.name) { + composable(ModuleRoute.AUDIO.name) { AudioConfigItemList( - audioConfig = moduleConfig.audio, + audioConfig = radioConfigState.moduleConfig.audio, enabled = connected, onSaveClicked = { audioInput -> val config = moduleConfig { audio = audioInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } ) } - composable(ModuleDest.REMOTE_HARDWARE.name) { + composable(ModuleRoute.REMOTE_HARDWARE.name) { RemoteHardwareConfigItemList( - remoteHardwareConfig = moduleConfig.remoteHardware, + remoteHardwareConfig = radioConfigState.moduleConfig.remoteHardware, enabled = connected, onSaveClicked = { remoteHardwareInput -> val config = moduleConfig { remoteHardware = remoteHardwareInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } ) } - composable(ModuleDest.NEIGHBOR_INFO.name) { + composable(ModuleRoute.NEIGHBOR_INFO.name) { NeighborInfoConfigItemList( - neighborInfoConfig = moduleConfig.neighborInfo, + neighborInfoConfig = radioConfigState.moduleConfig.neighborInfo, enabled = connected, onSaveClicked = { neighborInfoInput -> val config = moduleConfig { neighborInfo = neighborInfoInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } ) } - composable(ModuleDest.AMBIENT_LIGHTING.name) { + composable(ModuleRoute.AMBIENT_LIGHTING.name) { AmbientLightingConfigItemList( - ambientLightingConfig = moduleConfig.ambientLighting, + ambientLightingConfig = radioConfigState.moduleConfig.ambientLighting, enabled = connected, onSaveClicked = { ambientLightingInput -> val config = moduleConfig { ambientLighting = ambientLightingInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } ) } - composable(ModuleDest.DETECTION_SENSOR.name) { + composable(ModuleRoute.DETECTION_SENSOR.name) { DetectionSensorConfigItemList( - detectionSensorConfig = moduleConfig.detectionSensor, + detectionSensorConfig = radioConfigState.moduleConfig.detectionSensor, enabled = connected, onSaveClicked = { detectionSensorInput -> val config = moduleConfig { detectionSensor = detectionSensorInput } viewModel.setModuleConfig(destNum, config) - moduleConfig = config } ) } @@ -758,11 +643,6 @@ private fun NavCard( } } -@Composable -private fun NavCard(@StringRes title: Int, enabled: Boolean, onClick: () -> Unit) { - NavCard(title = stringResource(title), enabled = enabled, onClick = onClick) -} - @Composable private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Unit) { var showDialog by remember { mutableStateOf(false) } @@ -832,16 +712,10 @@ private fun RadioSettingsScreen( modifier = Modifier.padding(horizontal = 16.dp) ) { item { PreferenceCategory(stringResource(R.string.device_settings)) } - item { NavCard("User", enabled = enabled) { onRouteClick("USER") } } - item { NavCard("Channels", enabled = enabled) { onRouteClick("CHANNELS") } } - items(ConfigDest.values()) { config -> - NavCard(config.title, enabled = enabled) { onRouteClick(config) } - } + items(ConfigRoute.entries) { NavCard(it.title, enabled = enabled) { onRouteClick(it) } } item { PreferenceCategory(stringResource(R.string.module_settings)) } - items(ModuleDest.values()) { module -> - NavCard(module.title, enabled = enabled) { onRouteClick(module) } - } + items(ModuleRoute.entries) { NavCard(it.title, enabled = enabled) { onRouteClick(it) } } if (isLocal) { item { PreferenceCategory("Import / Export") } 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 666c46b70..62e8cd38d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -314,7 +314,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { model.channels.asLiveData().observe(viewLifecycleOwner) { if (!model.isConnected()) it.protobuf.let { ch -> - val maxChannels = model.myNodeInfo.value?.maxChannels ?: "8" + val maxChannels = model.maxChannels if (!ch.hasLoraConfig() && ch.settingsCount > 0) scanModel.setErrorText("Channels (${ch.settingsCount} / $maxChannels)") } diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt index 614b12192..f176a55da 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -332,24 +332,14 @@ class UsersFragment : ScreenFragment("Users"), Logging { } } - model.packetResponse.asLiveData().observe(viewLifecycleOwner) { meshLog -> - meshLog?.meshPacket?.let { meshPacket -> - val routeList = meshLog.routeDiscovery?.routeList ?: return@let - fun nodeName(num: Int) = model.nodeDB.nodesByNum?.get(num)?.user?.longName - ?: getString(R.string.unknown_username) + model.tracerouteResponse.observe(viewLifecycleOwner) { response -> + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.traceroute) + .setMessage(response ?: return@observe) + .setPositiveButton(R.string.okay) { _, _ -> } + .show() - var routeStr = "${nodeName(meshPacket.to)} --> " - routeList.forEach { num -> routeStr += "${nodeName(num)} --> " } - routeStr += nodeName(meshPacket.from) - - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.traceroute) - .setMessage(routeStr) - .setPositiveButton(R.string.okay) { _, _ -> } - .show() - - model.clearPacketResponse() - } + model.clearTracerouteResponse() } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt index 3683c7ee3..333d347b3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt @@ -14,24 +14,26 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.geeksville.mesh.R -import com.geeksville.mesh.ui.PacketResponseState +import com.geeksville.mesh.ui.ResponseState @Composable -fun PacketResponseStateDialog( - state: PacketResponseState, - onDismiss: () -> Unit +fun PacketResponseStateDialog( + state: ResponseState, + onDismiss: () -> Unit = {}, + onComplete: () -> Unit = {}, ) { AlertDialog( - onDismissRequest = { }, + onDismissRequest = {}, title = { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - if (state is PacketResponseState.Loading) { + if (state is ResponseState.Loading) { val progress = state.completed.toFloat() / state.total.toFloat() Text("%.0f%%".format(progress * 100)) LinearProgressIndicator( @@ -41,12 +43,14 @@ fun PacketResponseStateDialog( .padding(top = 8.dp), color = MaterialTheme.colors.onSurface, ) + if (state.total == state.completed) onComplete() } - if (state is PacketResponseState.Success) { + if (state is ResponseState.Success) { Text("Success!") } - if (state is PacketResponseState.Error) { - Text("Error: ${state.error}") + if (state is ResponseState.Error) { + Text(text = "Error\n", textAlign = TextAlign.Center) + Text(state.error) } } }, @@ -59,7 +63,7 @@ fun PacketResponseStateDialog( onClick = onDismiss, modifier = Modifier.padding(top = 16.dp) ) { - if (state is PacketResponseState.Loading) { + if (state is ResponseState.Loading) { Text(stringResource(R.string.cancel)) } else { Text(stringResource(R.string.close)) @@ -74,10 +78,9 @@ fun PacketResponseStateDialog( @Composable private fun PacketResponseStateDialogPreview() { PacketResponseStateDialog( - state = PacketResponseState.Loading.apply { - total = 17 - completed = 5 - }, - onDismiss = { } + state = ResponseState.Loading( + total = 17, + completed = 5, + ), ) }