mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: add remote node configuration (#626)
This commit is contained in:
parent
ec3a046fb6
commit
85e62eaab4
12 changed files with 523 additions and 267 deletions
|
|
@ -24,6 +24,7 @@ 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.ChannelSetRepository
|
||||
import com.geeksville.mesh.repository.datastore.LocalConfigRepository
|
||||
|
|
@ -34,7 +35,6 @@ import com.geeksville.mesh.util.positionToMeter
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -45,7 +45,6 @@ import kotlinx.coroutines.flow.mapLatest
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.osmdroid.bonuspack.kml.KmlDocument
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.FolderOverlay
|
||||
|
|
@ -120,6 +119,10 @@ class UIViewModel @Inject constructor(
|
|||
private val _ourNodeInfo = MutableStateFlow<NodeInfo?>(null)
|
||||
val ourNodeInfo: StateFlow<NodeInfo?> = _ourNodeInfo
|
||||
|
||||
private val requestId = MutableStateFlow<Int?>(null)
|
||||
private val _packetResponse = MutableStateFlow<MeshLog?>(null)
|
||||
val packetResponse: StateFlow<MeshLog?> = _packetResponse
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
meshLogRepository.getAllLogs().collect { logs ->
|
||||
|
|
@ -148,6 +151,13 @@ class UIViewModel @Inject constructor(
|
|||
combine(nodeDB.nodes.asFlow(), nodeDB.myId.asFlow()) { nodes, id -> nodes[id] }.onEach {
|
||||
_ourNodeInfo.value = it
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
combine(_meshLog, requestId) { packet, requestId ->
|
||||
requestId?.run { packet.lastOrNull { it.meshPacket?.decoded?.requestId == requestId } }
|
||||
}.onEach {
|
||||
_packetResponse.value = it
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
debug("ViewModel created")
|
||||
}
|
||||
|
||||
|
|
@ -174,42 +184,12 @@ class UIViewModel @Inject constructor(
|
|||
.filterValues { it.data.waypoint!!.expire > System.currentTimeMillis() / 1000 }
|
||||
}.asLiveData()
|
||||
|
||||
private val _packetResponse = MutableStateFlow<MeshLog?>(null)
|
||||
val packetResponse: StateFlow<MeshLog?> = _packetResponse
|
||||
|
||||
/**
|
||||
* Called immediately after activity observes packetResponse
|
||||
*/
|
||||
fun clearPacketResponse() {
|
||||
_packetResponse.tryEmit(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the packet response to a given [packetId] or null after [timeout] milliseconds
|
||||
*/
|
||||
private suspend fun getResponseBy(packetId: Int, timeout: Long) = withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(timeout) {
|
||||
var packet: MeshLog? = null
|
||||
while (packet == null) {
|
||||
packet = _meshLog.value.lastOrNull { it.meshPacket?.decoded?.requestId == packetId }
|
||||
if (packet == null) delay(1000)
|
||||
}
|
||||
packet
|
||||
}
|
||||
}
|
||||
|
||||
fun requestTraceroute(destNum: Int) = viewModelScope.launch {
|
||||
meshService?.let { service ->
|
||||
try {
|
||||
val packetId = service.packetId
|
||||
val waitFactor = (service.nodes.count { it.isOnline } - 1)
|
||||
.coerceAtMost(config.lora.hopLimit)
|
||||
service.requestTraceroute(packetId, destNum)
|
||||
_packetResponse.emit(getResponseBy(packetId, 20000L * waitFactor))
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("Request traceroute error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
requestId.value = null
|
||||
_packetResponse.value = null
|
||||
}
|
||||
|
||||
fun generatePacketId(): Int? {
|
||||
|
|
@ -247,6 +227,69 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun request(
|
||||
destNum: Int,
|
||||
requestAction: suspend (IMeshService, Int, Int) -> Unit,
|
||||
errorMessage: String,
|
||||
configType: Int = 0
|
||||
) = viewModelScope.launch {
|
||||
meshService?.let { service ->
|
||||
val packetId = service.packetId
|
||||
try {
|
||||
requestAction(service, packetId, destNum)
|
||||
requestId.value = packetId
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("$errorMessage: ${ex.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getOwner(destNum: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getRemoteOwner(packetId, dest) },
|
||||
"Request getOwner error"
|
||||
)
|
||||
|
||||
fun getConfig(destNum: Int, configType: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getRemoteConfig(packetId, dest, configType) },
|
||||
"Request getConfig error",
|
||||
configType
|
||||
)
|
||||
|
||||
fun getModuleConfig(destNum: Int, configType: Int) = request(
|
||||
destNum,
|
||||
{ service, packetId, dest -> service.getModuleConfig(packetId, dest, configType) },
|
||||
"Request getModuleConfig error",
|
||||
configType
|
||||
)
|
||||
|
||||
fun setRingtone(destNum: Int, ringtone: String) {
|
||||
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) {
|
||||
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 requestPosition(destNum: Int, position: Position = Position(0.0, 0.0, 0)) {
|
||||
try {
|
||||
meshService?.requestPosition(destNum, position)
|
||||
|
|
@ -325,10 +368,6 @@ class UIViewModel @Inject constructor(
|
|||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
val isRouter: Boolean = config.device.role == Config.DeviceConfig.Role.ROUTER
|
||||
|
||||
// We consider hasWifi = ESP32
|
||||
fun hasGPS() = myNodeInfo.value?.hasGPS == true
|
||||
fun hasWifi() = myNodeInfo.value?.hasWifi == true
|
||||
|
||||
/// hardware info about our local device (can be null)
|
||||
private val _myNodeInfo = MutableLiveData<MyNodeInfo?>()
|
||||
val myNodeInfo: LiveData<MyNodeInfo?> get() = _myNodeInfo
|
||||
|
|
@ -361,93 +400,22 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
inline fun updateDeviceConfig(crossinline body: (Config.DeviceConfig) -> Config.DeviceConfig) {
|
||||
val data = body(config.device)
|
||||
setConfig(config { device = data })
|
||||
}
|
||||
|
||||
inline fun updatePositionConfig(crossinline body: (Config.PositionConfig) -> Config.PositionConfig) {
|
||||
val data = body(config.position)
|
||||
setConfig(config { position = data })
|
||||
}
|
||||
|
||||
inline fun updatePowerConfig(crossinline body: (Config.PowerConfig) -> Config.PowerConfig) {
|
||||
val data = body(config.power)
|
||||
setConfig(config { power = data })
|
||||
}
|
||||
|
||||
inline fun updateNetworkConfig(crossinline body: (Config.NetworkConfig) -> Config.NetworkConfig) {
|
||||
val data = body(config.network)
|
||||
setConfig(config { network = data })
|
||||
}
|
||||
|
||||
inline fun updateDisplayConfig(crossinline body: (Config.DisplayConfig) -> Config.DisplayConfig) {
|
||||
val data = body(config.display)
|
||||
setConfig(config { display = data })
|
||||
}
|
||||
|
||||
inline fun updateLoraConfig(crossinline body: (Config.LoRaConfig) -> Config.LoRaConfig) {
|
||||
private inline fun updateLoraConfig(crossinline body: (Config.LoRaConfig) -> Config.LoRaConfig) {
|
||||
val data = body(config.lora)
|
||||
setConfig(config { lora = data })
|
||||
}
|
||||
|
||||
inline fun updateBluetoothConfig(crossinline body: (Config.BluetoothConfig) -> Config.BluetoothConfig) {
|
||||
val data = body(config.bluetooth)
|
||||
setConfig(config { bluetooth = data })
|
||||
}
|
||||
|
||||
// Set the radio config (also updates our saved copy in preferences)
|
||||
fun setConfig(config: Config) {
|
||||
meshService?.setConfig(config.toByteArray())
|
||||
}
|
||||
|
||||
inline fun updateMQTTConfig(crossinline body: (ModuleConfig.MQTTConfig) -> ModuleConfig.MQTTConfig) {
|
||||
val data = body(module.mqtt)
|
||||
setModuleConfig(moduleConfig { mqtt = data })
|
||||
fun setRemoteConfig(destNum: Int, config: Config) {
|
||||
meshService?.setRemoteConfig(destNum, config.toByteArray())
|
||||
}
|
||||
|
||||
inline fun updateSerialConfig(crossinline body: (ModuleConfig.SerialConfig) -> ModuleConfig.SerialConfig) {
|
||||
val data = body(module.serial)
|
||||
setModuleConfig(moduleConfig { serial = data })
|
||||
}
|
||||
|
||||
inline fun updateExternalNotificationConfig(crossinline body: (ModuleConfig.ExternalNotificationConfig) -> ModuleConfig.ExternalNotificationConfig) {
|
||||
val data = body(module.externalNotification)
|
||||
setModuleConfig(moduleConfig { externalNotification = data })
|
||||
}
|
||||
|
||||
inline fun updateStoreForwardConfig(crossinline body: (ModuleConfig.StoreForwardConfig) -> ModuleConfig.StoreForwardConfig) {
|
||||
val data = body(module.storeForward)
|
||||
setModuleConfig(moduleConfig { storeForward = data })
|
||||
}
|
||||
|
||||
inline fun updateRangeTestConfig(crossinline body: (ModuleConfig.RangeTestConfig) -> ModuleConfig.RangeTestConfig) {
|
||||
val data = body(module.rangeTest)
|
||||
setModuleConfig(moduleConfig { rangeTest = data })
|
||||
}
|
||||
|
||||
inline fun updateTelemetryConfig(crossinline body: (ModuleConfig.TelemetryConfig) -> ModuleConfig.TelemetryConfig) {
|
||||
val data = body(module.telemetry)
|
||||
setModuleConfig(moduleConfig { telemetry = data })
|
||||
}
|
||||
|
||||
inline fun updateCannedMessageConfig(crossinline body: (ModuleConfig.CannedMessageConfig) -> ModuleConfig.CannedMessageConfig) {
|
||||
val data = body(module.cannedMessage)
|
||||
setModuleConfig(moduleConfig { cannedMessage = data })
|
||||
}
|
||||
|
||||
inline fun updateAudioConfig(crossinline body: (ModuleConfig.AudioConfig) -> ModuleConfig.AudioConfig) {
|
||||
val data = body(module.audio)
|
||||
setModuleConfig(moduleConfig { audio = data })
|
||||
}
|
||||
|
||||
inline fun updateRemoteHardwareConfig(crossinline body: (ModuleConfig.RemoteHardwareConfig) -> ModuleConfig.RemoteHardwareConfig) {
|
||||
val data = body(module.remoteHardware)
|
||||
setModuleConfig(moduleConfig { remoteHardware = data })
|
||||
}
|
||||
|
||||
fun setModuleConfig(config: ModuleConfig) {
|
||||
meshService?.setModuleConfig(config.toByteArray())
|
||||
fun setModuleConfig(destNum: Int, config: ModuleConfig) {
|
||||
meshService?.setModuleConfig(destNum, config.toByteArray())
|
||||
}
|
||||
|
||||
/// Convert the channels array to and from [AppOnlyProtos.ChannelSet]
|
||||
|
|
@ -518,6 +486,15 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun setRemoteOwner(destNum: Int, user: User) {
|
||||
try {
|
||||
// Note: we use ?. here because we might be running in the emulator
|
||||
meshService?.setRemoteOwner(destNum, user.toByteArray())
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("Can't set username on device, is device offline? ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
val adminChannelIndex: Int
|
||||
get() = channelSet.settingsList.map { it.name.lowercase() }.indexOf("admin")
|
||||
|
||||
|
|
@ -584,7 +561,7 @@ class UIViewModel @Inject constructor(
|
|||
meshLogRepository.getAllLogsInReceiveOrder(Int.MAX_VALUE).first().forEach { packet ->
|
||||
// If we get a NodeInfo packet, use it to update our position data (if valid)
|
||||
packet.nodeInfo?.let { nodeInfo ->
|
||||
positionToPos.invoke(nodeInfo.position)?.let { _ ->
|
||||
positionToPos.invoke(nodeInfo.position)?.let {
|
||||
nodePositions[nodeInfo.num] = nodeInfo.position
|
||||
}
|
||||
}
|
||||
|
|
@ -592,7 +569,7 @@ class UIViewModel @Inject constructor(
|
|||
packet.meshPacket?.let { proto ->
|
||||
// If the packet contains position data then use it to update, if valid
|
||||
packet.position?.let { position ->
|
||||
positionToPos.invoke(position)?.let { _ ->
|
||||
positionToPos.invoke(position)?.let {
|
||||
nodePositions[proto.from] = position
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue