feat: add feedback for configuration changes

This commit is contained in:
andrekir 2023-10-03 18:05:40 -03:00 committed by Andre K
parent 2dd0e1f1e2
commit 7c30d86e39
6 changed files with 138 additions and 143 deletions

View file

@ -58,7 +58,7 @@ interface IMeshService {
*/
void setOwner(in MeshUser user);
void setRemoteOwner(in int destNum, in byte []payload);
void setRemoteOwner(in int requestId, in byte []payload);
void getRemoteOwner(in int requestId, in int destNum);
/// Return my unique user ID string
@ -91,11 +91,11 @@ interface IMeshService {
void setConfig(in byte []payload);
/// Set and get a Config protobuf via admin packet
void setRemoteConfig(in int destNum, in byte []payload);
void setRemoteConfig(in int requestId, in int destNum, in byte []payload);
void getRemoteConfig(in int requestId, in int destNum, in int configTypeValue);
/// Set and get a ModuleConfig protobuf via admin packet
void setModuleConfig(in int destNum, in byte []payload);
void setModuleConfig(in int requestId, in int destNum, in byte []payload);
void getModuleConfig(in int requestId, in int destNum, in int moduleConfigTypeValue);
/// Set and get the Ext Notification Ringtone string via admin packet
@ -111,7 +111,7 @@ interface IMeshService {
void setChannel(in byte []payload);
/// Set and get a Channel protobuf via admin packet
void setRemoteChannel(in int destNum, in byte []payload);
void setRemoteChannel(in int requestId, in int destNum, in byte []payload);
void getRemoteChannel(in int requestId, in int destNum, in int channelIndex);
/// Send beginEditSettings admin packet to nodeNum

View file

@ -135,7 +135,7 @@ class UIViewModel @Inject constructor(
private val _ourNodeInfo = MutableStateFlow<NodeInfo?>(null)
val ourNodeInfo: StateFlow<NodeInfo?> = _ourNodeInfo
private val requestId = MutableStateFlow<Int?>(null)
private val requestIds = MutableStateFlow<HashMap<Int, Boolean>>(hashMapOf())
private val _radioConfigState = MutableStateFlow(RadioConfigState())
val radioConfigState: StateFlow<RadioConfigState> = _radioConfigState
@ -169,11 +169,11 @@ class UIViewModel @Inject constructor(
}.launchIn(viewModelScope)
viewModelScope.launch {
combine(meshLogRepository.getAllLogs(9), requestId) { list, id ->
list.takeIf { id != null }?.firstOrNull { it.meshPacket?.decoded?.requestId == id }
}.collect(::processPacketResponse)
combine(meshLogRepository.getAllLogs(9), requestIds) { list, ids ->
val unprocessed = ids.filterValues { !it }.keys.ifEmpty { return@combine emptyList() }
list.filter { log -> log.meshPacket?.decoded?.requestId in unprocessed }
}.collect { it.forEach(::processPacketResponse) }
}
debug("ViewModel created")
}
@ -255,7 +255,15 @@ class UIViewModel @Inject constructor(
val packetId = service.packetId
try {
requestAction(service, packetId, destNum)
requestId.value = packetId
requestIds.update { it.apply { put(packetId, false) } }
_radioConfigState.update { state ->
if (state.responseState is ResponseState.Loading) {
val total = maxOf(requestIds.value.size, state.responseState.total)
state.copy(responseState = state.responseState.copy(total = total))
} else {
state.copy(responseState = ResponseState.Loading())
}
}
} catch (ex: RemoteException) {
errormsg("$errorMessage: ${ex.message}")
}
@ -268,18 +276,44 @@ class UIViewModel @Inject constructor(
"Request getOwner error"
)
private fun setRemoteChannel(destNum: Int, channel: ChannelProtos.Channel) = request(
destNum,
{ service, packetId, dest ->
service.setRemoteChannel(packetId, dest, channel.toByteArray())
},
"Request setRemoteChannel error"
)
fun getChannel(destNum: Int, index: Int) = request(
destNum,
{ service, packetId, dest -> service.getRemoteChannel(packetId, dest, index) },
"Request getChannel error"
)
fun setRemoteConfig(destNum: Int, config: Config) = request(
destNum,
{ service, packetId, dest ->
_radioConfigState.update { it.copy(radioConfig = config) }
service.setRemoteConfig(packetId, dest, config.toByteArray())
},
"Request setConfig error",
)
fun getConfig(destNum: Int, configType: Int) = request(
destNum,
{ service, packetId, dest -> service.getRemoteConfig(packetId, dest, configType) },
"Request getConfig error",
)
fun setModuleConfig(destNum: Int, config: ModuleConfig) = request(
destNum,
{ service, packetId, dest ->
_radioConfigState.update { it.copy(moduleConfig = config) }
service.setModuleConfig(packetId, dest, config.toByteArray())
},
"Request setConfig error",
)
fun getModuleConfig(destNum: Int, configType: Int) = request(
destNum,
{ service, packetId, dest -> service.getModuleConfig(packetId, dest, configType) },
@ -472,16 +506,6 @@ class UIViewModel @Inject constructor(
meshService?.setConfig(config.toByteArray())
}
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())
}
fun setModuleConfig(config: ModuleConfig) {
setModuleConfig(myNodeNum ?: return, config)
}
@ -515,6 +539,7 @@ class UIViewModel @Inject constructor(
if (destNum == myNodeNum) viewModelScope.launch {
radioConfigRepository.replaceAllSettings(new)
}
_radioConfigState.update { it.copy(channelList = new) }
}
private fun updateChannels(
@ -542,14 +567,6 @@ class UIViewModel @Inject constructor(
this._channelSet = channelSet.protobuf
}
private fun setRemoteChannel(destNum: Int, channel: ChannelProtos.Channel) {
try {
meshService?.setRemoteChannel(destNum, channel.toByteArray())
} catch (ex: RemoteException) {
errormsg("Can't set channel on radio ${ex.message}")
}
}
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean("provide-location", false)) {
override fun setValue(value: Boolean) {
super.setValue(value)
@ -564,15 +581,14 @@ class UIViewModel @Inject constructor(
setRemoteOwner(myNodeNum ?: return, user)
}
fun setRemoteOwner(destNum: Int, user: User) {
try {
// Note: we use ?. here because we might be running in the emulator
meshService?.setRemoteOwner(destNum, user.toByteArray())
fun setRemoteOwner(destNum: Int, user: User) = request(
destNum,
{ service, packetId, _ ->
_radioConfigState.update { it.copy(userConfig = user) }
} catch (ex: RemoteException) {
errormsg("Can't set username on device, is device offline? ${ex.message}")
}
}
service.setRemoteOwner(packetId, user.toByteArray())
},
"Request setOwner error",
)
val adminChannelIndex: Int /** matches [MeshService.adminChannelIndex] **/
get() = channelSet.settingsList.indexOfFirst { it.name.equals("admin", ignoreCase = true) }
@ -723,6 +739,7 @@ class UIViewModel @Inject constructor(
message.writeTo(outputStream)
}
}
setResponseStateSuccess()
} catch (ex: Exception) {
val error = "${ex.javaClass.simpleName}: ${ex.message}"
errormsg("Can't write file error: ${ex.message}")
@ -763,6 +780,7 @@ class UIViewModel @Inject constructor(
setModuleConfig(moduleConfig { audio = it.audio })
setModuleConfig(moduleConfig { remoteHardware = it.remoteHardware })
}
setResponseStateSuccess()
// meshService?.commitEditSettings()
}
@ -806,17 +824,20 @@ class UIViewModel @Inject constructor(
}
fun clearPacketResponse() {
requestIds.value = hashMapOf()
_radioConfigState.update { it.copy(responseState = ResponseState.Empty) }
}
fun setResponseStateLoading(route: String) {
_radioConfigState.value = RadioConfigState(
route = route,
responseState = ResponseState.Loading(total = 1),
responseState = ResponseState.Loading(),
)
// channel editor is synchronous, so we don't use requestIds as total
if (route == ConfigRoute.CHANNELS.name) setResponseStateTotal(maxChannels + 1)
}
fun setResponseStateTotal(total: Int) {
private fun setResponseStateTotal(total: Int) {
_radioConfigState.update { state ->
if (state.responseState is ResponseState.Loading) {
state.copy(responseState = state.responseState.copy(total = total))
@ -826,6 +847,16 @@ class UIViewModel @Inject constructor(
}
}
private fun setResponseStateSuccess() {
_radioConfigState.update { state ->
if (state.responseState is ResponseState.Loading) {
state.copy(responseState = ResponseState.Success(true))
} else {
state // Return the unchanged state for other response states
}
}
}
private fun setResponseStateError(error: String) {
_radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) }
}
@ -841,14 +872,6 @@ class UIViewModel @Inject constructor(
}
}
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
@ -859,8 +882,7 @@ class UIViewModel @Inject constructor(
private fun processPacketResponse(log: MeshLog?) {
val packet = log?.meshPacket ?: return
val data = packet.decoded
val fromStr = packet.from.toUInt()
requestId.value = null
requestIds.update { it.apply { put(data.requestId, true) } }
if (data?.portnumValue == Portnums.PortNum.TRACEROUTE_APP_VALUE) {
val parsed = MeshProtos.RouteDiscovery.parseFrom(data.payload)
@ -874,25 +896,26 @@ class UIViewModel @Inject constructor(
}
}
val destNum = destNode.value?.num ?: return
val destStr = destNum.toUInt()
val debugMsg = "requestId: ${data.requestId.toUInt()} to: ${destNum.toUInt()} received %s from: ${packet.from.toUInt()}"
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")
debug(debugMsg.format(parsed.errorReason.name))
if (parsed.errorReason != MeshProtos.Routing.Error.NONE) {
setResponseStateError(parsed.errorReason.name)
} else if (packet.from == destNum) {
_radioConfigState.update { it.copy(responseState = ResponseState.Success(true)) }
if (requestIds.value.filterValues { !it }.isEmpty()) setResponseStateSuccess()
else incrementCompleted()
}
}
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")
debug(debugMsg.format(parsed.payloadVariantCase.name))
if (destNum != packet.from) {
setResponseStateError("Unexpected sender: $fromStr instead of $destStr.")
setResponseStateError("Unexpected sender: ${packet.from.toUInt()} instead of ${destNum.toUInt()}.")
return
}
// check destination: lora config or channel editor
// check if destination is channel editor
val goChannels = radioConfigState.value.route == ConfigRoute.CHANNELS.name
when (parsed.payloadVariantCase) {
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
@ -900,23 +923,18 @@ class UIViewModel @Inject constructor(
// 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 {
state.copy(channelList = state.channelList.toMutableList().apply {
add(response.index, response.settings)
}
state.copy(channelList = updatedList)
})
}
incrementCompleted()
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)
// Received last channel, update total and start channel editor
setResponseStateTotal(response.index + 1)
getConfig(destNum, AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE)
}
}
@ -948,13 +966,11 @@ class UIViewModel @Inject constructor(
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

@ -1483,35 +1483,23 @@ class MeshService : Service(), Logging {
/**
* Send setOwner admin packet with [MeshProtos.User] protobuf
*/
fun setOwner(meshUser: MeshUser) = with(meshUser) {
private fun setOwner(packetId: Int, user: MeshProtos.User) = with(user) {
val dest = nodeDBbyID[id]
if (dest != null) {
val old = dest.user
if (longName == old?.longName && shortName == old.shortName && isLicensed == old.isLicensed)
debug("Ignoring nop owner change")
else {
debug("SetOwner Id: $id longName: ${longName.anonymize} shortName: $shortName isLicensed: $isLicensed")
?: throw Exception("Can't set user without a NodeInfo") // this shouldn't happen
val old = dest.user!!
if (longName == old.longName && shortName == old.shortName && isLicensed == old.isLicensed) {
debug("Ignoring nop owner change")
} else {
debug("setOwner Id: $id longName: ${longName.anonymize} shortName: $shortName isLicensed: $isLicensed")
val user = MeshProtos.User.newBuilder().also {
it.longName = longName
it.shortName = shortName
it.hwModel = hwModel
it.isLicensed = isLicensed
}.build()
// Also update our own map for our nodeNum, by handling the packet just like packets from other users
handleReceivedUser(dest.num, user)
// Also update our own map for our nodenum, by handling the packet just like packets from other users
handleReceivedUser(dest.num, user)
// encapsulate our payload in the proper protobufs and fire it off
val packet = newMeshPacketTo(dest.num).buildAdminPacket {
setOwner = user
}
// send the packet into the mesh
sendToRadio(packet)
}
} else
throw Exception("Can't set user without a node info") // this shouldn't happen
// encapsulate our payload in the proper protobuf and fire it off
sendToRadio(newMeshPacketTo(dest.num).buildAdminPacket(id = packetId) {
setOwner = user
})
}
}
@ -1614,14 +1602,12 @@ class MeshService : Service(), Logging {
override fun getPacketId() = toRemoteExceptions { generatePacketId() }
override fun setOwner(user: MeshUser) = toRemoteExceptions {
this@MeshService.setOwner(user)
setOwner(generatePacketId(), user.toProto())
}
override fun setRemoteOwner(destNum: Int, payload: ByteArray) = toRemoteExceptions {
override fun setRemoteOwner(id: Int, payload: ByteArray) = toRemoteExceptions {
val parsed = MeshProtos.User.parseFrom(payload)
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket {
setOwner = parsed
})
setOwner(id, parsed)
}
override fun getRemoteOwner(id: Int, destNum: Int) = toRemoteExceptions {
@ -1674,14 +1660,14 @@ class MeshService : Service(), Logging {
/** Send our current radio config to the device
*/
override fun setConfig(payload: ByteArray) = toRemoteExceptions {
setRemoteConfig(myNodeNum, payload)
setRemoteConfig(generatePacketId(), myNodeNum, payload)
}
override fun setRemoteConfig(destNum: Int, payload: ByteArray) = toRemoteExceptions {
override fun setRemoteConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
debug("Setting new radio config!")
val config = ConfigProtos.Config.parseFrom(payload)
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { setConfig = config })
if (destNum == myNodeNum) setLocalConfig(config) // Update our local copy
sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setConfig = config })
if (num == myNodeNum) setLocalConfig(config) // Update our local copy
}
override fun getRemoteConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions {
@ -1692,11 +1678,11 @@ class MeshService : Service(), Logging {
/** Send our current module config to the device
*/
override fun setModuleConfig(destNum: Int, payload: ByteArray) = toRemoteExceptions {
override fun setModuleConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions {
debug("Setting new module config!")
val config = ModuleConfigProtos.ModuleConfig.parseFrom(payload)
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { setModuleConfig = config })
if (destNum == myNodeNum) setLocalModuleConfig(config) // Update our local copy
sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setModuleConfig = config })
if (num == myNodeNum) setLocalModuleConfig(config) // Update our local copy
}
override fun getModuleConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions {
@ -1730,12 +1716,12 @@ class MeshService : Service(), Logging {
}
override fun setChannel(payload: ByteArray?) = toRemoteExceptions {
setRemoteChannel(myNodeNum, payload)
setRemoteChannel(generatePacketId(), myNodeNum, payload)
}
override fun setRemoteChannel(destNum: Int, payload: ByteArray?) = toRemoteExceptions {
override fun setRemoteChannel(id: Int, num: Int, payload: ByteArray?) = toRemoteExceptions {
val channel = ChannelProtos.Channel.parseFrom(payload)
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { setChannel = channel })
sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setChannel = channel })
}
override fun getRemoteChannel(id: Int, destNum: Int, index: Int) = toRemoteExceptions {

View file

@ -191,7 +191,7 @@ private fun getName(route: Any): String = when (route) {
*/
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 Loading(var total: Int = 1, var completed: Int = 0) : ResponseState<Nothing>()
data class Success<T>(val result: T) : ResponseState<T>()
data class Error(val error: String) : ResponseState<Nothing>()
}
@ -231,7 +231,6 @@ fun RadioConfigNavHost(
val destNum = node?.num ?: 0
val isLocal = destNum == viewModel.myNodeNum
val maxChannels = viewModel.maxChannels
val radioConfigState by viewModel.radioConfigState.collectAsStateWithLifecycle()
var location by remember(node) { mutableStateOf(node?.position) } // FIXME
@ -314,12 +313,15 @@ fun RadioConfigNavHost(
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)
ConfigRoute.USER -> {
viewModel.getOwner(destNum)
}
ConfigRoute.CHANNELS -> {
viewModel.getChannel(destNum, 0)
viewModel.getConfig(destNum, ConfigRoute.LORA.configType)
}
"IMPORT" -> {
viewModel.clearPacketResponse()
viewModel.setDeviceProfile(null)
@ -329,6 +331,7 @@ fun RadioConfigNavHost(
}
importConfigLauncher.launch(intent)
}
"EXPORT" -> {
viewModel.clearPacketResponse()
showEditDeviceProfileDialog = true
@ -350,23 +353,20 @@ fun RadioConfigNavHost(
viewModel.requestNodedbReset(destNum)
}
ConfigRoute.LORA -> {
viewModel.setResponseStateTotal(2)
viewModel.clearRemoteChannelList()
viewModel.getChannel(destNum, 0)
}
is ConfigRoute -> {
if (route == ConfigRoute.LORA) {
viewModel.getChannel(destNum, 0)
}
viewModel.getConfig(destNum, route.configType)
}
ModuleRoute.CANNED_MESSAGE -> {
viewModel.setResponseStateTotal(2)
viewModel.getCannedMessages(destNum)
}
ModuleRoute.EXTERNAL_NOTIFICATION -> {
viewModel.setResponseStateTotal(2)
viewModel.getRingtone(destNum)
}
is ModuleRoute -> {
if (route == ModuleRoute.CANNED_MESSAGE) {
viewModel.getCannedMessages(destNum)
}
if (route == ModuleRoute.EXTERNAL_NOTIFICATION) {
viewModel.getRingtone(destNum)
}
viewModel.getModuleConfig(destNum, route.configType)
}
}
@ -387,10 +387,9 @@ fun RadioConfigNavHost(
settingsList = radioConfigState.channelList,
modemPresetName = Channel(loraConfig = radioConfigState.radioConfig.lora).name,
enabled = connected,
maxChannels = maxChannels,
maxChannels = viewModel.maxChannels,
onPositiveClicked = { channelListInput ->
viewModel.updateChannels(destNum, radioConfigState.channelList, channelListInput)
viewModel.setRemoteChannelList(channelListInput)
},
)
}

View file

@ -28,10 +28,10 @@ import androidx.compose.material.icons.twotone.Add
import androidx.compose.material.icons.twotone.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -94,9 +94,7 @@ fun ChannelSettingsItemList(
onPositiveClicked: (List<ChannelSettings>) -> Unit,
) {
val focusManager = LocalFocusManager.current
val settingsListInput = remember {
mutableStateListOf<ChannelSettings>().apply { addAll(settingsList) }
}
val settingsListInput = remember(settingsList) { settingsList.toMutableStateList() }
val isEditing: Boolean = settingsList.size != settingsListInput.size
|| settingsList.zip(settingsListInput).any { (item1, item2) -> item1 != item2 }
@ -172,10 +170,12 @@ fun ChannelSettingsItemList(
) {
FloatingActionButton(
onClick = {
settingsListInput.add(channelSettings {
psk = Channel.default.settings.psk
})
showEditChannelDialog = settingsListInput.lastIndex
if (maxChannels > settingsListInput.size) {
settingsListInput.add(channelSettings {
psk = Channel.default.settings.psk
})
showEditChannelDialog = settingsListInput.lastIndex
}
},
modifier = Modifier.padding(16.dp)
) { Icon(Icons.TwoTone.Add, stringResource(R.string.add)) }

View file

@ -51,7 +51,7 @@ fun <T> PacketResponseStateDialog(
if (state.total == state.completed) onComplete()
}
if (state is ResponseState.Success) {
Text("Success!")
Text("Delivery confirmed.")
}
if (state is ResponseState.Error) {
Text(text = "Error\n", textAlign = TextAlign.Center)
@ -67,13 +67,7 @@ fun <T> PacketResponseStateDialog(
Button(
onClick = onDismiss,
modifier = Modifier.padding(top = 16.dp)
) {
if (state is ResponseState.Loading) {
Text(stringResource(R.string.cancel))
} else {
Text(stringResource(R.string.close))
}
}
) { Text(stringResource(R.string.close)) }
}
}
)