mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(config): move business logic to ViewModel
This commit is contained in:
parent
e8b0f1584b
commit
7869243290
8 changed files with 354 additions and 323 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue