refactor(config): move business logic to ViewModel

This commit is contained in:
andrekir 2023-09-16 09:51:16 -03:00 committed by Andre K
parent e8b0f1584b
commit 7869243290
8 changed files with 354 additions and 323 deletions

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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<ChannelSettings> = emptyList(),
val radioConfig: Config = Config.getDefaultInstance(),
val moduleConfig: ModuleConfig = ModuleConfig.getDefaultInstance(),
val ringtone: String = "",
val cannedMessageMessages: String = "",
val responseState: ResponseState<Boolean> = ResponseState.Empty,
)
@HiltViewModel
class UIViewModel @Inject constructor(
private val app: Application,
@ -119,8 +136,8 @@ class UIViewModel @Inject constructor(
val ourNodeInfo: StateFlow<NodeInfo?> = _ourNodeInfo
private val requestId = MutableStateFlow<Int?>(null)
private val _packetResponse = MutableStateFlow<MeshLog?>(null)
val packetResponse: StateFlow<MeshLog?> = _packetResponse
private val _radioConfigState = MutableStateFlow(RadioConfigState())
val radioConfigState: StateFlow<RadioConfigState> = _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<MyNodeInfo?>()
val myNodeInfo: LiveData<MyNodeInfo?> 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<Int, MeshProtos.Position?>()
@ -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<ChannelSettings>) {
_radioConfigState.update { it.copy(channelList = list) }
}
private val _tracerouteResponse = MutableLiveData<String?>(null)
val tracerouteResponse: LiveData<String?> 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()
}
}
}
}

View file

@ -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) {

View file

@ -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<ConfigDest>().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<String>) : PacketResponseState()
object Empty : PacketResponseState()
data class Error(val error: String) : PacketResponseState()
sealed class ResponseState<out T> {
data object Empty : ResponseState<Nothing>()
data class Loading(var total: Int = 0, var completed: Int = 0) : ResponseState<Nothing>()
data class Success<T>(val result: T) : ResponseState<T>()
data class Error(val error: String) : ResponseState<Nothing>()
}
@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<ChannelProtos.ChannelSettings>() }
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>(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<ConfigDest>().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<ModuleDest>().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") }

View file

@ -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)")
}

View file

@ -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()
}
}

View file

@ -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 <T> PacketResponseStateDialog(
state: ResponseState<T>,
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,
),
)
}