mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: move RadioConfig logic into separate ViewModel
This commit is contained in:
parent
37dad9b6fa
commit
3288b07e5e
6 changed files with 598 additions and 434 deletions
|
|
@ -0,0 +1,512 @@
|
|||
package com.geeksville.mesh.model
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.RemoteException
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.AdminProtos
|
||||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.ModuleConfigProtos
|
||||
import com.geeksville.mesh.MyNodeInfo
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.deviceProfile
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import com.geeksville.mesh.service.MeshService.ConnectionState
|
||||
import com.geeksville.mesh.ui.ConfigRoute
|
||||
import com.geeksville.mesh.ui.ResponseState
|
||||
import com.google.protobuf.MessageLite
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Data class that represents the current RadioConfig state.
|
||||
*/
|
||||
data class RadioConfigState(
|
||||
val route: String = "",
|
||||
val userConfig: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
|
||||
val channelList: List<ChannelProtos.ChannelSettings> = emptyList(),
|
||||
val radioConfig: ConfigProtos.Config = ConfigProtos.Config.getDefaultInstance(),
|
||||
val moduleConfig: ModuleConfigProtos.ModuleConfig = ModuleConfigProtos.ModuleConfig.getDefaultInstance(),
|
||||
val ringtone: String = "",
|
||||
val cannedMessageMessages: String = "",
|
||||
val responseState: ResponseState<Boolean> = ResponseState.Empty,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class RadioConfigViewModel @Inject constructor(
|
||||
private val app: Application,
|
||||
radioInterfaceService: RadioInterfaceService,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
meshLogRepository: MeshLogRepository,
|
||||
) : ViewModel(), Logging {
|
||||
|
||||
private var destNum: Int = 0
|
||||
private val meshService: IMeshService? get() = radioConfigRepository.meshService
|
||||
|
||||
// Connection state to our radio device
|
||||
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
|
||||
val connectionState: StateFlow<ConnectionState> get() = _connectionState
|
||||
|
||||
// A map from nodeNum to NodeInfo
|
||||
private val _nodes = MutableStateFlow<Map<Int, NodeInfo>>(mapOf())
|
||||
val nodes: StateFlow<Map<Int, NodeInfo>> = _nodes
|
||||
|
||||
private val _myNodeInfo = MutableStateFlow<MyNodeInfo?>(null)
|
||||
val myNodeInfo get() = _myNodeInfo
|
||||
|
||||
private val requestIds = MutableStateFlow<HashMap<Int, Boolean>>(hashMapOf())
|
||||
private val _radioConfigState = MutableStateFlow(RadioConfigState())
|
||||
val radioConfigState: StateFlow<RadioConfigState> = _radioConfigState
|
||||
|
||||
private val _currentDeviceProfile = MutableStateFlow(deviceProfile {})
|
||||
val currentDeviceProfile get() = _currentDeviceProfile.value
|
||||
|
||||
init {
|
||||
radioInterfaceService.connectionState.onEach { state ->
|
||||
_connectionState.value = when {
|
||||
state.isConnected -> ConnectionState.CONNECTED
|
||||
else -> ConnectionState.DISCONNECTED
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
radioConfigRepository.myNodeInfoFlow().onEach {
|
||||
_myNodeInfo.value = it
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
radioConfigRepository.nodeInfoFlow().onEach { list ->
|
||||
_nodes.value = list.associateBy { it.num }
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
radioConfigRepository.deviceProfileFlow.onEach {
|
||||
_currentDeviceProfile.value = it
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
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("RadioConfigViewModel created")
|
||||
}
|
||||
|
||||
val myNodeNum get() = _myNodeInfo.value?.myNodeNum
|
||||
val maxChannels get() = _myNodeInfo.value?.maxChannels ?: 8
|
||||
private val ourNodeInfo: NodeInfo? get() = nodes.value[myNodeNum]
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
debug("RadioConfigViewModel cleared")
|
||||
}
|
||||
|
||||
private fun request(
|
||||
destNum: Int,
|
||||
requestAction: suspend (IMeshService, Int, Int) -> Unit,
|
||||
errorMessage: String,
|
||||
) = viewModelScope.launch {
|
||||
meshService?.let { service ->
|
||||
this@RadioConfigViewModel.destNum = destNum
|
||||
val packetId = service.packetId
|
||||
try {
|
||||
requestAction(service, packetId, destNum)
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setOwner(user: MeshProtos.User) {
|
||||
setRemoteOwner(myNodeNum ?: return, user)
|
||||
}
|
||||
|
||||
fun setRemoteOwner(destNum: Int, user: MeshProtos.User) = request(
|
||||
destNum,
|
||||
{ service, packetId, _ ->
|
||||
_radioConfigState.update { it.copy(userConfig = user) }
|
||||
service.setRemoteOwner(packetId, user.toByteArray())
|
||||
},
|
||||
"Request setOwner error",
|
||||
)
|
||||
|
||||
fun getOwner(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getRemoteOwner(packetId, dest) },
|
||||
"Request getOwner error"
|
||||
)
|
||||
|
||||
fun updateChannels(
|
||||
destNum: Int,
|
||||
new: List<ChannelProtos.ChannelSettings>,
|
||||
old: List<ChannelProtos.ChannelSettings>,
|
||||
) {
|
||||
getChannelList(new, old).forEach { setRemoteChannel(destNum, it) }
|
||||
|
||||
if (destNum == myNodeNum) viewModelScope.launch {
|
||||
radioConfigRepository.replaceAllSettings(new)
|
||||
}
|
||||
_radioConfigState.update { it.copy(channelList = new) }
|
||||
}
|
||||
|
||||
private fun setChannels(channelUrl: String) = viewModelScope.launch {
|
||||
val new = ChannelSet(Uri.parse(channelUrl)).protobuf
|
||||
val old = radioConfigRepository.channelSetFlow.firstOrNull() ?: return@launch
|
||||
updateChannels(myNodeNum ?: return@launch, new.settingsList, old.settingsList)
|
||||
}
|
||||
|
||||
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: ConfigProtos.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: ModuleConfigProtos.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) },
|
||||
"Request getModuleConfig error",
|
||||
)
|
||||
|
||||
fun setRingtone(destNum: Int, ringtone: String) {
|
||||
_radioConfigState.update { it.copy(ringtone = ringtone) }
|
||||
meshService?.setRingtone(destNum, ringtone)
|
||||
}
|
||||
|
||||
fun getRingtone(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getRingtone(packetId, dest) },
|
||||
"Request getRingtone error"
|
||||
)
|
||||
|
||||
fun setCannedMessages(destNum: Int, messages: String) {
|
||||
_radioConfigState.update { it.copy(cannedMessageMessages = messages) }
|
||||
meshService?.setCannedMessages(destNum, messages)
|
||||
}
|
||||
|
||||
fun getCannedMessages(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getCannedMessages(packetId, dest) },
|
||||
"Request getCannedMessages error"
|
||||
)
|
||||
|
||||
fun requestShutdown(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.requestShutdown(packetId, dest) },
|
||||
"Request shutdown error"
|
||||
)
|
||||
|
||||
fun requestReboot(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.requestReboot(packetId, dest) },
|
||||
"Request reboot error"
|
||||
)
|
||||
|
||||
fun requestFactoryReset(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.requestFactoryReset(packetId, dest) },
|
||||
"Request factory reset error"
|
||||
)
|
||||
|
||||
fun requestNodedbReset(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.requestNodedbReset(packetId, dest) },
|
||||
"Request NodeDB reset error"
|
||||
)
|
||||
|
||||
fun requestPosition(destNum: Int, position: Position = Position(0.0, 0.0, 0)) {
|
||||
try {
|
||||
meshService?.requestPosition(destNum, position)
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("Request position error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Set the radio config (also updates our saved copy in preferences)
|
||||
fun setConfig(config: ConfigProtos.Config) {
|
||||
setRemoteConfig(myNodeNum ?: return, config)
|
||||
}
|
||||
|
||||
fun setModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
|
||||
setModuleConfig(myNodeNum ?: return, config)
|
||||
}
|
||||
|
||||
private val _deviceProfile = MutableStateFlow<DeviceProfile?>(null)
|
||||
val deviceProfile get() = _deviceProfile.value
|
||||
|
||||
fun setDeviceProfile(deviceProfile: DeviceProfile?) {
|
||||
_deviceProfile.value = deviceProfile
|
||||
}
|
||||
|
||||
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(uri: Uri) = viewModelScope.launch {
|
||||
val profile = deviceProfile ?: return@launch
|
||||
writeToUri(uri, profile)
|
||||
_deviceProfile.value = null
|
||||
}
|
||||
|
||||
private suspend fun writeToUri(uri: Uri, message: MessageLite) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).use { outputStream ->
|
||||
message.writeTo(outputStream)
|
||||
}
|
||||
}
|
||||
setResponseStateSuccess()
|
||||
} catch (ex: Exception) {
|
||||
val error = "${ex.javaClass.simpleName}: ${ex.message}"
|
||||
errormsg("Can't write file error: ${ex.message}")
|
||||
setResponseStateError(error)
|
||||
}
|
||||
}
|
||||
|
||||
fun installProfile(protobuf: DeviceProfile) = with(protobuf) {
|
||||
_deviceProfile.value = null
|
||||
// meshService?.beginEditSettings()
|
||||
if (hasLongName() || hasShortName()) ourNodeInfo?.user?.let {
|
||||
val user = it.copy(
|
||||
longName = if (hasLongName()) longName else it.longName,
|
||||
shortName = if (hasShortName()) shortName else it.shortName
|
||||
)
|
||||
setOwner(user.toProto())
|
||||
}
|
||||
if (hasChannelUrl()) {
|
||||
setChannels(channelUrl)
|
||||
}
|
||||
if (hasConfig()) {
|
||||
setConfig(config { device = config.device })
|
||||
setConfig(config { position = config.position })
|
||||
setConfig(config { power = config.power })
|
||||
setConfig(config { network = config.network })
|
||||
setConfig(config { display = config.display })
|
||||
setConfig(config { lora = config.lora })
|
||||
setConfig(config { bluetooth = config.bluetooth })
|
||||
}
|
||||
if (hasModuleConfig()) moduleConfig.let {
|
||||
setModuleConfig(moduleConfig { mqtt = it.mqtt })
|
||||
setModuleConfig(moduleConfig { serial = it.serial })
|
||||
setModuleConfig(moduleConfig { externalNotification = it.externalNotification })
|
||||
setModuleConfig(moduleConfig { storeForward = it.storeForward })
|
||||
setModuleConfig(moduleConfig { rangeTest = it.rangeTest })
|
||||
setModuleConfig(moduleConfig { telemetry = it.telemetry })
|
||||
setModuleConfig(moduleConfig { cannedMessage = it.cannedMessage })
|
||||
setModuleConfig(moduleConfig { audio = it.audio })
|
||||
setModuleConfig(moduleConfig { remoteHardware = it.remoteHardware })
|
||||
}
|
||||
setResponseStateSuccess()
|
||||
// meshService?.commitEditSettings()
|
||||
}
|
||||
|
||||
fun clearPacketResponse() {
|
||||
requestIds.value = hashMapOf()
|
||||
_radioConfigState.update { it.copy(responseState = ResponseState.Empty) }
|
||||
}
|
||||
|
||||
fun setResponseStateLoading(route: String) {
|
||||
_radioConfigState.value = RadioConfigState(
|
||||
route = route,
|
||||
responseState = ResponseState.Loading(),
|
||||
)
|
||||
// channel editor is synchronous, so we don't use requestIds as total
|
||||
if (route == ConfigRoute.CHANNELS.name) setResponseStateTotal(maxChannels + 1)
|
||||
}
|
||||
|
||||
private 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 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)) }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processPacketResponse(log: MeshLog?) {
|
||||
val packet = log?.meshPacket ?: return
|
||||
val data = packet.decoded
|
||||
requestIds.update { it.apply { put(data.requestId, true) } }
|
||||
|
||||
// val destNum = destNode.value?.num ?: return
|
||||
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(debugMsg.format(parsed.errorReason.name))
|
||||
if (parsed.errorReason != MeshProtos.Routing.Error.NONE) {
|
||||
setResponseStateError(parsed.errorReason.name)
|
||||
} else if (packet.from == destNum) {
|
||||
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(debugMsg.format(parsed.payloadVariantCase.name))
|
||||
if (destNum != packet.from) {
|
||||
setResponseStateError("Unexpected sender: ${packet.from.toUInt()} instead of ${destNum.toUInt()}.")
|
||||
return
|
||||
}
|
||||
// check if destination is channel editor
|
||||
val goChannels = radioConfigState.value.route == ConfigRoute.CHANNELS.name
|
||||
when (parsed.payloadVariantCase) {
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
|
||||
val response = parsed.getChannelResponse
|
||||
// Stop once we get to the first disabled entry
|
||||
if (response.role != ChannelProtos.Channel.Role.DISABLED) {
|
||||
_radioConfigState.update { state ->
|
||||
state.copy(channelList = state.channelList.toMutableList().apply {
|
||||
add(response.index, response.settings)
|
||||
})
|
||||
}
|
||||
incrementCompleted()
|
||||
if (response.index + 1 < maxChannels && goChannels) {
|
||||
// Not done yet, request next channel
|
||||
getChannel(destNum, response.index + 1)
|
||||
}
|
||||
} else {
|
||||
// Received last channel, update total and start channel editor
|
||||
setResponseStateTotal(response.index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_RINGTONE_RESPONSE -> {
|
||||
_radioConfigState.update { it.copy(ringtone = parsed.getRingtoneResponse) }
|
||||
incrementCompleted()
|
||||
}
|
||||
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.ChannelProtos.ChannelSettings
|
||||
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
|
||||
import com.geeksville.mesh.ConfigProtos.Config
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import com.geeksville.mesh.database.QuickChatActionRepository
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
|
|
@ -25,15 +23,11 @@ import com.geeksville.mesh.database.entity.MeshLog
|
|||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import com.geeksville.mesh.MeshProtos.User
|
||||
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
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -50,7 +44,6 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedWriter
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.FileWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
|
@ -81,18 +74,29 @@ fun getInitials(nameIn: String): String {
|
|||
}
|
||||
|
||||
/**
|
||||
* Data class that represents the current RadioConfig state.
|
||||
* Builds a [Channel] list from the difference between two [ChannelSettings] lists.
|
||||
* Only changes are included in the resulting list.
|
||||
*
|
||||
* @param new The updated [ChannelSettings] list.
|
||||
* @param old The current [ChannelSettings] list (required to disable unused channels).
|
||||
* @return A [Channel] list containing only the modified channels.
|
||||
*/
|
||||
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,
|
||||
)
|
||||
internal fun getChannelList(
|
||||
new: List<ChannelSettings>,
|
||||
old: List<ChannelSettings>,
|
||||
): List<ChannelProtos.Channel> = buildList {
|
||||
for (i in 0..maxOf(old.lastIndex, new.lastIndex)) {
|
||||
if (old.getOrNull(i) != new.getOrNull(i)) add(channel {
|
||||
role = when (i) {
|
||||
0 -> ChannelProtos.Channel.Role.PRIMARY
|
||||
in 1..new.lastIndex -> ChannelProtos.Channel.Role.SECONDARY
|
||||
else -> ChannelProtos.Channel.Role.DISABLED
|
||||
}
|
||||
index = i
|
||||
settings = new.getOrNull(i) ?: channelSettings { }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
class UIViewModel @Inject constructor(
|
||||
|
|
@ -136,8 +140,6 @@ class UIViewModel @Inject constructor(
|
|||
val ourNodeInfo: StateFlow<NodeInfo?> = _ourNodeInfo
|
||||
|
||||
private val requestIds = MutableStateFlow<HashMap<Int, Boolean>>(hashMapOf())
|
||||
private val _radioConfigState = MutableStateFlow(RadioConfigState())
|
||||
val radioConfigState: StateFlow<RadioConfigState> = _radioConfigState
|
||||
|
||||
init {
|
||||
radioConfigRepository.nodeInfoFlow().onEach(nodeDB::setNodes)
|
||||
|
|
@ -246,132 +248,16 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun request(
|
||||
destNum: Int,
|
||||
requestAction: suspend (IMeshService, Int, Int) -> Unit,
|
||||
errorMessage: String,
|
||||
) = viewModelScope.launch {
|
||||
meshService?.let { service ->
|
||||
val packetId = service.packetId
|
||||
try {
|
||||
requestAction(service, packetId, destNum)
|
||||
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}")
|
||||
}
|
||||
fun requestTraceroute(destNum: Int) {
|
||||
try {
|
||||
val packetId = meshService?.packetId ?: return
|
||||
meshService?.requestTraceroute(packetId, destNum)
|
||||
requestIds.update { it.apply { put(packetId, false) } }
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("Request traceroute error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getOwner(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getRemoteOwner(packetId, dest) },
|
||||
"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) },
|
||||
"Request getModuleConfig error",
|
||||
)
|
||||
|
||||
fun setRingtone(destNum: Int, ringtone: String) {
|
||||
_radioConfigState.update { it.copy(ringtone = ringtone) }
|
||||
meshService?.setRingtone(destNum, ringtone)
|
||||
}
|
||||
|
||||
fun getRingtone(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getRingtone(packetId, dest) },
|
||||
"Request getRingtone error"
|
||||
)
|
||||
|
||||
fun setCannedMessages(destNum: Int, messages: String) {
|
||||
_radioConfigState.update { it.copy(cannedMessageMessages = messages) }
|
||||
meshService?.setCannedMessages(destNum, messages)
|
||||
}
|
||||
|
||||
fun getCannedMessages(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getCannedMessages(packetId, dest) },
|
||||
"Request getCannedMessages error"
|
||||
)
|
||||
|
||||
fun requestTraceroute(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.requestTraceroute(packetId, dest) },
|
||||
"Request traceroute error"
|
||||
)
|
||||
|
||||
fun requestShutdown(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.requestShutdown(packetId, dest) },
|
||||
"Request shutdown error"
|
||||
)
|
||||
|
||||
fun requestReboot(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.requestReboot(packetId, dest) },
|
||||
"Request reboot error"
|
||||
)
|
||||
|
||||
fun requestFactoryReset(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.requestFactoryReset(packetId, dest) },
|
||||
"Request factory reset error"
|
||||
)
|
||||
|
||||
fun requestNodedbReset(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.requestNodedbReset(packetId, dest) },
|
||||
"Request NodeDB reset error"
|
||||
)
|
||||
|
||||
fun requestPosition(destNum: Int, position: Position = Position(0.0, 0.0, 0)) {
|
||||
try {
|
||||
meshService?.requestPosition(destNum, position)
|
||||
|
|
@ -506,47 +392,8 @@ class UIViewModel @Inject constructor(
|
|||
meshService?.setConfig(config.toByteArray())
|
||||
}
|
||||
|
||||
fun setModuleConfig(config: ModuleConfig) {
|
||||
setModuleConfig(myNodeNum ?: return, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates channels to match the [new] list. Only channels with changes are updated.
|
||||
*
|
||||
* @param destNum Destination node number.
|
||||
* @param old The current [ChannelSettings] list.
|
||||
* @param new The updated [ChannelSettings] list.
|
||||
*/
|
||||
fun updateChannels(
|
||||
destNum: Int,
|
||||
old: List<ChannelSettings>,
|
||||
new: List<ChannelSettings>,
|
||||
) {
|
||||
buildList {
|
||||
for (i in 0..maxOf(old.lastIndex, new.lastIndex)) {
|
||||
if (old.getOrNull(i) != new.getOrNull(i)) add(channel {
|
||||
role = when (i) {
|
||||
0 -> ChannelProtos.Channel.Role.PRIMARY
|
||||
in 1..new.lastIndex -> ChannelProtos.Channel.Role.SECONDARY
|
||||
else -> ChannelProtos.Channel.Role.DISABLED
|
||||
}
|
||||
index = i
|
||||
settings = new.getOrNull(i) ?: channelSettings { }
|
||||
})
|
||||
}
|
||||
}.forEach { setRemoteChannel(destNum, it) }
|
||||
|
||||
if (destNum == myNodeNum) viewModelScope.launch {
|
||||
radioConfigRepository.replaceAllSettings(new)
|
||||
}
|
||||
_radioConfigState.update { it.copy(channelList = new) }
|
||||
}
|
||||
|
||||
private fun updateChannels(
|
||||
old: List<ChannelSettings>,
|
||||
new: List<ChannelSettings>
|
||||
) {
|
||||
updateChannels(myNodeNum ?: return, old, new)
|
||||
fun setChannel(channel: ChannelProtos.Channel) {
|
||||
meshService?.setChannel(channel.toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -555,10 +402,15 @@ class UIViewModel @Inject constructor(
|
|||
private var _channelSet: AppOnlyProtos.ChannelSet
|
||||
get() = channels.value.protobuf
|
||||
set(value) {
|
||||
updateChannels(channelSet.settingsList, value.settingsList)
|
||||
val new = value.settingsList
|
||||
val old = channelSet.settingsList
|
||||
viewModelScope.launch {
|
||||
getChannelList(new, old).forEach(::setChannel)
|
||||
radioConfigRepository.replaceAllSettings(new)
|
||||
|
||||
val newConfig = config { lora = value.loraConfig }
|
||||
if (config.lora != newConfig.lora) setConfig(newConfig)
|
||||
val newConfig = config { lora = value.loraConfig }
|
||||
if (config.lora != newConfig.lora) setConfig(newConfig)
|
||||
}
|
||||
}
|
||||
val channelSet get() = _channelSet
|
||||
|
||||
|
|
@ -577,19 +429,15 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun setOwner(user: User) {
|
||||
setRemoteOwner(myNodeNum ?: return, user)
|
||||
fun setOwner(user: MeshUser) {
|
||||
try {
|
||||
// Note: we use ?. here because we might be running in the emulator
|
||||
meshService?.setOwner(user)
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("Can't set username on device, is device offline? ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun setRemoteOwner(destNum: Int, user: User) = request(
|
||||
destNum,
|
||||
{ service, packetId, _ ->
|
||||
_radioConfigState.update { it.copy(userConfig = user) }
|
||||
service.setRemoteOwner(packetId, user.toByteArray())
|
||||
},
|
||||
"Request setOwner error",
|
||||
)
|
||||
|
||||
val adminChannelIndex: Int /** matches [MeshService.adminChannelIndex] **/
|
||||
get() = channelSet.settingsList.indexOfFirst { it.name.equals("admin", ignoreCase = true) }
|
||||
.coerceAtLeast(0)
|
||||
|
|
@ -705,85 +553,6 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private val _deviceProfile = MutableStateFlow<DeviceProfile?>(null)
|
||||
val deviceProfile: StateFlow<DeviceProfile?> = _deviceProfile
|
||||
|
||||
fun setDeviceProfile(deviceProfile: DeviceProfile?) {
|
||||
_deviceProfile.value = deviceProfile
|
||||
}
|
||||
|
||||
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(uri: Uri) = viewModelScope.launch {
|
||||
val profile = deviceProfile.value ?: return@launch
|
||||
writeToUri(uri, profile)
|
||||
_deviceProfile.value = null
|
||||
}
|
||||
|
||||
private suspend fun writeToUri(uri: Uri, message: MessageLite) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).use { outputStream ->
|
||||
message.writeTo(outputStream)
|
||||
}
|
||||
}
|
||||
setResponseStateSuccess()
|
||||
} catch (ex: Exception) {
|
||||
val error = "${ex.javaClass.simpleName}: ${ex.message}"
|
||||
errormsg("Can't write file error: ${ex.message}")
|
||||
setResponseStateError(error)
|
||||
}
|
||||
}
|
||||
|
||||
fun installProfile(protobuf: DeviceProfile) = with(protobuf) {
|
||||
_deviceProfile.value = null
|
||||
// meshService?.beginEditSettings()
|
||||
if (hasLongName() || hasShortName()) ourNodeInfo.value?.user?.let {
|
||||
val user = it.copy(
|
||||
longName = if (hasLongName()) longName else it.longName,
|
||||
shortName = if (hasShortName()) shortName else it.shortName
|
||||
)
|
||||
setOwner(user.toProto())
|
||||
}
|
||||
if (hasChannelUrl()) {
|
||||
setChannels(ChannelSet(Uri.parse(channelUrl)))
|
||||
}
|
||||
if (hasConfig()) {
|
||||
setConfig(config { device = config.device })
|
||||
setConfig(config { position = config.position })
|
||||
setConfig(config { power = config.power })
|
||||
setConfig(config { network = config.network })
|
||||
setConfig(config { display = config.display })
|
||||
setConfig(config { lora = config.lora })
|
||||
setConfig(config { bluetooth = config.bluetooth })
|
||||
}
|
||||
if (hasModuleConfig()) moduleConfig.let {
|
||||
setModuleConfig(moduleConfig { mqtt = it.mqtt })
|
||||
setModuleConfig(moduleConfig { serial = it.serial })
|
||||
setModuleConfig(moduleConfig { externalNotification = it.externalNotification })
|
||||
setModuleConfig(moduleConfig { storeForward = it.storeForward })
|
||||
setModuleConfig(moduleConfig { rangeTest = it.rangeTest })
|
||||
setModuleConfig(moduleConfig { telemetry = it.telemetry })
|
||||
setModuleConfig(moduleConfig { cannedMessage = it.cannedMessage })
|
||||
setModuleConfig(moduleConfig { audio = it.audio })
|
||||
setModuleConfig(moduleConfig { remoteHardware = it.remoteHardware })
|
||||
}
|
||||
setResponseStateSuccess()
|
||||
// meshService?.commitEditSettings()
|
||||
}
|
||||
|
||||
fun addQuickChatAction(name: String, value: String, mode: QuickChatAction.Mode) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val action = QuickChatAction(0, name, value, mode, _quickChatActions.value.size)
|
||||
|
|
@ -823,55 +592,6 @@ 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(),
|
||||
)
|
||||
// channel editor is synchronous, so we don't use requestIds as total
|
||||
if (route == ConfigRoute.CHANNELS.name) setResponseStateTotal(maxChannels + 1)
|
||||
}
|
||||
|
||||
private 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 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)) }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _tracerouteResponse = MutableLiveData<String?>(null)
|
||||
val tracerouteResponse: LiveData<String?> get() = _tracerouteResponse
|
||||
|
||||
|
|
@ -895,86 +615,5 @@ class UIViewModel @Inject constructor(
|
|||
append(nodeName(packet.from))
|
||||
}
|
||||
}
|
||||
val destNum = destNode.value?.num ?: return
|
||||
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(debugMsg.format(parsed.errorReason.name))
|
||||
if (parsed.errorReason != MeshProtos.Routing.Error.NONE) {
|
||||
setResponseStateError(parsed.errorReason.name)
|
||||
} else if (packet.from == destNum) {
|
||||
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(debugMsg.format(parsed.payloadVariantCase.name))
|
||||
if (destNum != packet.from) {
|
||||
setResponseStateError("Unexpected sender: ${packet.from.toUInt()} instead of ${destNum.toUInt()}.")
|
||||
return
|
||||
}
|
||||
// check if destination is channel editor
|
||||
val goChannels = radioConfigState.value.route == ConfigRoute.CHANNELS.name
|
||||
when (parsed.payloadVariantCase) {
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
|
||||
val response = parsed.getChannelResponse
|
||||
// Stop once we get to the first disabled entry
|
||||
if (response.role != ChannelProtos.Channel.Role.DISABLED) {
|
||||
_radioConfigState.update { state ->
|
||||
state.copy(channelList = state.channelList.toMutableList().apply {
|
||||
add(response.index, response.settings)
|
||||
})
|
||||
}
|
||||
incrementCompleted()
|
||||
if (response.index + 1 < maxChannels && goChannels) {
|
||||
// Not done yet, request next channel
|
||||
getChannel(destNum, response.index + 1)
|
||||
}
|
||||
} else {
|
||||
// Received last channel, update total and start channel editor
|
||||
setResponseStateTotal(response.index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_RINGTONE_RESPONSE -> {
|
||||
_radioConfigState.update { it.copy(ringtone = parsed.getRingtoneResponse) }
|
||||
incrementCompleted()
|
||||
}
|
||||
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.geeksville.mesh.repository.datastore
|
|||
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
||||
import com.geeksville.mesh.ChannelProtos.Channel
|
||||
import com.geeksville.mesh.ChannelProtos.ChannelSettings
|
||||
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
|
||||
import com.geeksville.mesh.ConfigProtos.Config
|
||||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
|
|
@ -12,9 +13,11 @@ import com.geeksville.mesh.MyNodeInfo
|
|||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.database.dao.MyNodeInfoDao
|
||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.deviceProfile
|
||||
import com.geeksville.mesh.service.ServiceRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
|
@ -139,4 +142,25 @@ class RadioConfigRepository @Inject constructor(
|
|||
suspend fun setLocalModuleConfig(config: ModuleConfig) {
|
||||
moduleConfigRepository.setLocalModuleConfig(config)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flow representing the combined [DeviceProfile] protobuf.
|
||||
*/
|
||||
val deviceProfileFlow: Flow<DeviceProfile> = combine(
|
||||
myNodeInfoFlow(),
|
||||
nodeInfoFlow(),
|
||||
channelSetFlow,
|
||||
localConfigFlow,
|
||||
moduleConfigFlow,
|
||||
) { myInfo, nodes, channels, localConfig, localModuleConfig ->
|
||||
deviceProfile {
|
||||
nodes.firstOrNull { it.num == myInfo?.myNodeNum }?.user?.let {
|
||||
longName = it.longName
|
||||
shortName = it.shortName
|
||||
}
|
||||
channelUrl = com.geeksville.mesh.model.ChannelSet(channels).getChannelUrl().toString()
|
||||
config = localConfig
|
||||
moduleConfig = localModuleConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ import androidx.compose.material.icons.filled.ArrowBack
|
|||
import androidx.compose.material.icons.twotone.KeyboardArrowRight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
|
@ -50,8 +49,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
|
|
@ -60,11 +59,11 @@ import com.geeksville.mesh.NodeInfo
|
|||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.deviceProfile
|
||||
import com.geeksville.mesh.model.Channel
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.service.MeshService.ConnectionState
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.config.AmbientLightingConfigItemList
|
||||
import com.geeksville.mesh.ui.components.config.AudioConfigItemList
|
||||
|
|
@ -137,9 +136,8 @@ class DeviceSettingsFragment : ScreenFragment("Radio Configuration"), Logging {
|
|||
}
|
||||
) { innerPadding ->
|
||||
RadioConfigNavHost(
|
||||
node,
|
||||
model,
|
||||
navController,
|
||||
node = node,
|
||||
navController = navController,
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
)
|
||||
}
|
||||
|
|
@ -222,20 +220,20 @@ private fun MeshAppBar(
|
|||
@Composable
|
||||
fun RadioConfigNavHost(
|
||||
node: NodeInfo?,
|
||||
viewModel: UIViewModel = viewModel(),
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val connectionState by viewModel.connectionState.observeAsState()
|
||||
val connected = connectionState == MeshService.ConnectionState.CONNECTED && node != null
|
||||
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val connected = connectionState == ConnectionState.CONNECTED && node != null
|
||||
|
||||
val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle() // FIXME
|
||||
val destNum = node?.num ?: 0
|
||||
val isLocal = destNum == viewModel.myNodeNum
|
||||
val isLocal = destNum == myNodeInfo?.myNodeNum
|
||||
|
||||
val radioConfigState by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
var location by remember(node) { mutableStateOf(node?.position) } // FIXME
|
||||
|
||||
val deviceProfile by viewModel.deviceProfile.collectAsStateWithLifecycle()
|
||||
val isWaiting = radioConfigState.responseState !is ResponseState.Empty
|
||||
var showEditDeviceProfileDialog by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -256,19 +254,10 @@ fun RadioConfigNavHost(
|
|||
}
|
||||
}
|
||||
|
||||
val deviceProfile = viewModel.deviceProfile
|
||||
if (showEditDeviceProfileDialog) EditDeviceProfileDialog(
|
||||
title = if (deviceProfile != null) "Import configuration" else "Export configuration",
|
||||
deviceProfile = deviceProfile ?: with(viewModel) {
|
||||
deviceProfile {
|
||||
ourNodeInfo.value?.user?.let {
|
||||
longName = it.longName
|
||||
shortName = it.shortName
|
||||
}
|
||||
channelUrl = channels.value.getChannelUrl().toString()
|
||||
config = localConfig.value
|
||||
this.moduleConfig = module
|
||||
}
|
||||
},
|
||||
deviceProfile = deviceProfile ?: viewModel.currentDeviceProfile,
|
||||
onAddClick = {
|
||||
showEditDeviceProfileDialog = false
|
||||
if (deviceProfile != null) {
|
||||
|
|
@ -389,7 +378,7 @@ fun RadioConfigNavHost(
|
|||
enabled = connected,
|
||||
maxChannels = viewModel.maxChannels,
|
||||
onPositiveClicked = { channelListInput ->
|
||||
viewModel.updateChannels(destNum, radioConfigState.channelList, channelListInput)
|
||||
viewModel.updateChannels(destNum, channelListInput, radioConfigState.channelList)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
val n = binding.usernameEditText.text.toString().trim()
|
||||
model.ourNodeInfo.value?.user?.let {
|
||||
val user = it.copy(longName = n, shortName = getInitials(n))
|
||||
if (n.isNotEmpty()) model.setOwner(user.toProto())
|
||||
if (n.isNotEmpty()) model.setOwner(user)
|
||||
}
|
||||
requireActivity().hideKeyboard()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue