Meshtastic-Android/app/src/main/java/com/geeksville/mesh/model/UIState.kt

799 lines
30 KiB
Kotlin
Raw Normal View History

2020-02-17 13:34:52 -08:00
package com.geeksville.mesh.model
import android.app.Application
2020-02-18 10:40:02 -08:00
import android.content.Context
2020-03-02 08:41:16 -08:00
import android.content.SharedPreferences
2020-03-17 11:35:19 -07:00
import android.net.Uri
2020-02-18 12:22:45 -08:00
import android.os.RemoteException
2020-07-17 17:06:29 -04:00
import android.view.Menu
2020-02-18 10:40:02 -08:00
import androidx.core.content.edit
2023-02-02 17:13:44 -03:00
import androidx.lifecycle.asFlow
2022-04-22 17:22:06 -03:00
import androidx.lifecycle.LiveData
2020-04-07 17:42:31 -07:00
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
2022-09-15 22:24:04 -03:00
import androidx.lifecycle.asLiveData
2020-09-23 22:47:45 -04:00
import androidx.lifecycle.viewModelScope
2022-09-04 22:52:40 -03:00
import com.geeksville.mesh.android.Logging
2022-02-03 18:15:06 -08:00
import com.geeksville.mesh.*
import com.geeksville.mesh.ChannelProtos.ChannelSettings
2023-05-02 07:18:22 -03:00
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
2022-09-12 00:26:12 -03:00
import com.geeksville.mesh.ConfigProtos.Config
2022-11-22 22:01:37 -03:00
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
2022-09-13 22:49:38 -03:00
import com.geeksville.mesh.database.MeshLogRepository
2022-08-11 16:43:26 +01:00
import com.geeksville.mesh.database.QuickChatActionRepository
2022-09-14 01:54:13 -03:00
import com.geeksville.mesh.database.entity.Packet
2022-09-13 22:49:38 -03:00
import com.geeksville.mesh.database.entity.MeshLog
2022-08-11 16:43:26 +01:00
import com.geeksville.mesh.database.entity.QuickChatAction
2022-09-12 00:26:12 -03:00
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
2022-11-22 22:01:37 -03:00
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.MeshProtos.User
2022-09-14 01:54:13 -03:00
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
2022-08-30 17:25:11 -03:00
import com.geeksville.mesh.util.GPSFormat
2022-05-20 11:20:13 -03:00
import com.geeksville.mesh.util.positionToMeter
2023-05-02 07:18:22 -03:00
import com.google.protobuf.MessageLite
import dagger.hilt.android.lifecycle.HiltViewModel
2020-09-23 22:47:45 -04:00
import kotlinx.coroutines.Dispatchers
2022-09-15 22:24:04 -03:00
import kotlinx.coroutines.ExperimentalCoroutinesApi
2023-02-02 17:13:44 -03:00
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
2022-02-03 18:15:06 -08:00
import kotlinx.coroutines.flow.first
2022-09-15 22:24:04 -03:00
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
2022-09-15 22:24:04 -03:00
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
2020-09-23 22:47:45 -04:00
import kotlinx.coroutines.launch
2022-02-03 18:15:06 -08:00
import kotlinx.coroutines.withContext
2022-09-22 08:35:33 -04:00
import org.osmdroid.bonuspack.kml.KmlDocument
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.FolderOverlay
2022-02-03 18:15:06 -08:00
import java.io.BufferedWriter
2022-02-13 08:09:26 -03:00
import java.io.FileNotFoundException
2023-05-02 07:18:22 -03:00
import java.io.FileOutputStream
2022-02-03 18:15:06 -08:00
import java.io.FileWriter
2023-05-02 07:18:22 -03:00
import java.io.InputStream
2022-02-03 18:15:06 -08:00
import java.text.SimpleDateFormat
2022-10-16 12:40:05 -03:00
import java.util.Locale
import javax.inject.Inject
2022-02-03 18:15:06 -08:00
import kotlin.math.roundToInt
/// Given a human name, strip out the first letter of the first three words and return that as the initials for
/// that user. If the original name is only one word, strip vowels from the original name and if the result is
/// 3 or more characters, use the first three characters. If not, just take the first 3 characters of the
/// original name.
fun getInitials(nameIn: String): String {
2022-11-15 12:19:59 -03:00
val nchars = 4
val minchars = 2
val name = nameIn.trim()
val words = name.split(Regex("\\s+")).filter { it.isNotEmpty() }
val initials = when (words.size) {
2022-05-26 16:23:47 -03:00
in 0 until minchars -> {
val nm = if (name.isNotEmpty())
2022-01-24 18:23:09 -03:00
name.first() + name.drop(1).filterNot { c -> c.lowercase() in "aeiou" }
2020-09-27 14:05:10 -07:00
else
""
if (nm.length >= nchars) nm else name
}
else -> words.map { it.first() }.joinToString("")
}
return initials.take(nchars)
}
2020-03-15 16:30:12 -07:00
@HiltViewModel
class UIViewModel @Inject constructor(
private val app: Application,
private val radioConfigRepository: RadioConfigRepository,
private val radioInterfaceService: RadioInterfaceService,
2022-09-13 22:49:38 -03:00
private val meshLogRepository: MeshLogRepository,
2022-09-14 01:54:13 -03:00
private val packetRepository: PacketRepository,
2022-08-11 16:43:26 +01:00
private val quickChatActionRepository: QuickChatActionRepository,
private val preferences: SharedPreferences
) : ViewModel(), Logging {
2020-09-23 22:47:45 -04:00
2023-02-02 17:13:44 -03:00
var actionBarMenu: Menu? = null
var meshService: IMeshService? = null
val nodeDB = NodeDB(this)
val bondedAddress get() = radioInterfaceService.getBondedDeviceAddress()
val selectedBluetooth get() = radioInterfaceService.getDeviceAddress()?.getOrNull(0) == 'x'
2022-09-13 22:49:38 -03:00
private val _meshLog = MutableStateFlow<List<MeshLog>>(emptyList())
val meshLog: StateFlow<List<MeshLog>> = _meshLog
2020-09-23 22:47:45 -04:00
2022-09-14 01:54:13 -03:00
private val _packets = MutableStateFlow<List<Packet>>(emptyList())
val packets: StateFlow<List<Packet>> = _packets
2022-09-12 00:26:12 -03:00
private val _localConfig = MutableStateFlow<LocalConfig>(LocalConfig.getDefaultInstance())
val localConfig: StateFlow<LocalConfig> = _localConfig
val config get() = _localConfig.value
2022-06-20 22:46:45 -03:00
2022-11-22 22:01:37 -03:00
private val _moduleConfig = MutableStateFlow<LocalModuleConfig>(LocalModuleConfig.getDefaultInstance())
val moduleConfig: StateFlow<LocalModuleConfig> = _moduleConfig
val module get() = _moduleConfig.value
2022-09-12 19:07:30 -03:00
private val _channels = MutableStateFlow(ChannelSet())
val channels: StateFlow<ChannelSet> = _channels
2022-08-23 21:39:08 -03:00
private val _quickChatActions = MutableStateFlow<List<QuickChatAction>>(emptyList())
2022-08-11 16:43:26 +01:00
val quickChatActions: StateFlow<List<QuickChatAction>> = _quickChatActions
2023-02-02 17:13:44 -03:00
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
2020-04-07 17:42:31 -07:00
init {
viewModelScope.launch {
2022-09-13 22:49:38 -03:00
meshLogRepository.getAllLogs().collect { logs ->
_meshLog.value = logs
2022-06-11 18:36:57 -03:00
}
2022-06-17 02:00:18 -03:00
}
2022-09-14 01:54:13 -03:00
viewModelScope.launch {
2022-09-15 22:24:04 -03:00
packetRepository.getAllPackets().collect { packets ->
_packets.value = packets
2022-09-14 01:54:13 -03:00
}
}
radioConfigRepository.localConfigFlow.onEach { config ->
_localConfig.value = config
}.launchIn(viewModelScope)
radioConfigRepository.moduleConfigFlow.onEach { config ->
2022-11-22 22:01:37 -03:00
_moduleConfig.value = config
}.launchIn(viewModelScope)
2022-08-11 16:43:26 +01:00
viewModelScope.launch {
quickChatActionRepository.getAllActions().collect { actions ->
_quickChatActions.value = actions
}
}
radioConfigRepository.channelSetFlow.onEach { channelSet ->
_channels.value = ChannelSet(channelSet)
}.launchIn(viewModelScope)
2023-02-02 17:13:44 -03:00
combine(nodeDB.nodes.asFlow(), nodeDB.myId.asFlow()) { nodes, id -> nodes[id] }.onEach {
_ourNodeInfo.value = it
}.launchIn(viewModelScope)
combine(meshLog, requestId) { packet, requestId ->
if (requestId != null) _packetResponse.value =
packet.firstOrNull { it.meshPacket?.decoded?.requestId == requestId }
}.launchIn(viewModelScope)
2020-04-07 17:42:31 -07:00
debug("ViewModel created")
}
2022-09-15 22:24:04 -03:00
private val contactKey: MutableStateFlow<String> = MutableStateFlow(DataPacket.ID_BROADCAST)
fun setContactKey(contact: String) {
contactKey.value = contact
}
@OptIn(ExperimentalCoroutinesApi::class)
val messages: LiveData<List<Packet>> = contactKey.flatMapLatest { contactKey ->
packetRepository.getMessagesFrom(contactKey)
}.asLiveData()
@OptIn(ExperimentalCoroutinesApi::class)
val contacts: LiveData<Map<String, Packet>> = _packets.mapLatest { list ->
2023-02-03 19:07:15 -03:00
list.filter { it.port_num == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE }
.associateBy { packet -> packet.contact_key }
2022-09-15 22:24:04 -03:00
}.asLiveData()
@OptIn(ExperimentalCoroutinesApi::class)
2023-02-03 19:07:15 -03:00
val waypoints: LiveData<Map<Int, Packet>> = _packets.mapLatest { list ->
list.filter { it.port_num == Portnums.PortNum.WAYPOINT_APP_VALUE }
.associateBy { packet -> packet.data.waypoint!!.id }
.filterValues { it.data.waypoint!!.expire > System.currentTimeMillis() / 1000 }
2022-09-15 22:24:04 -03:00
}.asLiveData()
2023-04-16 06:16:41 -03:00
/**
* Called immediately after activity observes packetResponse
*/
fun clearPacketResponse() {
requestId.value = null
_packetResponse.value = null
2023-04-16 06:16:41 -03:00
}
2023-02-01 12:16:44 -03:00
fun generatePacketId(): Int? {
return try {
meshService?.packetId
} catch (ex: RemoteException) {
errormsg("RemoteException: ${ex.message}")
return null
}
}
2023-01-02 21:36:35 -03:00
fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
// contactKey: unique contact key filter (channel)+(nodeId)
val channel = contactKey[0].digitToIntOrNull()
val dest = if (channel != null) contactKey.substring(1) else contactKey
val p = DataPacket(dest, channel ?: 0, str)
2023-02-01 12:16:44 -03:00
sendDataPacket(p)
}
fun sendWaypoint(wpt: MeshProtos.Waypoint, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
// contactKey: unique contact key filter (channel)+(nodeId)
val channel = contactKey[0].digitToIntOrNull()
val dest = if (channel != null) contactKey.substring(1) else contactKey
val p = DataPacket(dest, channel ?: 0, wpt)
2023-02-03 19:07:15 -03:00
if (wpt.id != 0) sendDataPacket(p)
2023-02-01 12:16:44 -03:00
}
private fun sendDataPacket(p: DataPacket) {
2022-09-15 22:24:04 -03:00
try {
meshService?.send(p)
} catch (ex: RemoteException) {
errormsg("Send DataPacket error: ${ex.message}")
}
}
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"
)
2023-04-29 07:14:30 -03:00
fun getChannel(destNum: Int, index: Int) = request(
destNum,
{ service, packetId, dest -> service.getRemoteChannel(packetId, dest, index) },
"Request getChannel 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 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}")
}
}
2022-09-13 22:49:38 -03:00
fun deleteAllLogs() = viewModelScope.launch(Dispatchers.IO) {
meshLogRepository.deleteAll()
2020-09-23 22:47:45 -04:00
}
2022-09-15 22:24:04 -03:00
fun deleteAllMessages() = viewModelScope.launch(Dispatchers.IO) {
packetRepository.deleteAllMessages()
}
fun deleteMessages(uuidList: List<Long>) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.deleteMessages(uuidList)
}
2023-02-01 12:16:44 -03:00
fun deleteWaypoint(id: Int) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.deleteWaypoint(id)
}
2020-04-07 17:42:31 -07:00
companion object {
fun getPreferences(context: Context): SharedPreferences =
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
}
2022-04-22 17:22:06 -03:00
/// Connection state to our radio device
private val _connectionState = MutableLiveData(MeshService.ConnectionState.DISCONNECTED)
2022-04-24 12:12:13 -03:00
val connectionState: LiveData<MeshService.ConnectionState> get() = _connectionState
2020-04-07 17:42:31 -07:00
2023-02-01 12:16:44 -03:00
fun isConnected() = _connectionState.value != MeshService.ConnectionState.DISCONNECTED
2020-04-07 17:42:31 -07:00
2022-04-22 17:22:06 -03:00
fun setConnectionState(connectionState: MeshService.ConnectionState) {
_connectionState.value = connectionState
2021-02-27 11:44:05 +08:00
}
2022-05-17 17:29:21 -03:00
private val _requestChannelUrl = MutableLiveData<Uri?>(null)
val requestChannelUrl: LiveData<Uri?> get() = _requestChannelUrl
fun setRequestChannelUrl(channelUrl: Uri) {
_requestChannelUrl.value = channelUrl
}
/**
* Called immediately after activity observes requestChannelUrl
*/
fun clearRequestChannelUrl() {
_requestChannelUrl.value = null
}
2022-09-18 18:35:13 -03:00
var txEnabled: Boolean
get() = config.lora.txEnabled
set(value) {
updateLoraConfig { it.copy { txEnabled = value } }
}
2022-09-12 00:26:12 -03:00
var region: Config.LoRaConfig.RegionCode
2022-09-18 18:35:13 -03:00
get() = config.lora.region
set(value) {
2022-09-12 00:26:12 -03:00
updateLoraConfig { it.copy { region = value } }
2021-02-05 21:29:28 -08:00
}
2022-09-18 18:35:13 -03:00
fun gpsString(p: Position): String {
return when (config.display.gpsFormat) {
Config.DisplayConfig.GpsCoordinateFormat.DEC -> GPSFormat.DEC(p)
Config.DisplayConfig.GpsCoordinateFormat.DMS -> GPSFormat.DMS(p)
Config.DisplayConfig.GpsCoordinateFormat.UTM -> GPSFormat.UTM(p)
Config.DisplayConfig.GpsCoordinateFormat.MGRS -> GPSFormat.MGRS(p)
else -> GPSFormat.DEC(p)
2022-08-30 17:25:11 -03:00
}
}
2023-05-13 18:18:49 -03:00
// managed mode disables all access to configuration
val isManaged: Boolean get() = config.device.isManaged
2022-06-17 02:00:18 -03:00
2021-03-04 09:08:29 +08:00
/// hardware info about our local device (can be null)
2022-04-22 17:22:06 -03:00
private val _myNodeInfo = MutableLiveData<MyNodeInfo?>()
val myNodeInfo: LiveData<MyNodeInfo?> get() = _myNodeInfo
2023-02-03 19:07:15 -03:00
val myNodeNum get() = _myNodeInfo.value?.myNodeNum
2022-04-22 17:22:06 -03:00
fun setMyNodeInfo(info: MyNodeInfo?) {
_myNodeInfo.value = info
}
2020-05-13 17:00:23 -07:00
override fun onCleared() {
super.onCleared()
debug("ViewModel cleared")
}
2022-04-22 17:22:06 -03:00
/// Pull our latest node db from the device
fun updateNodesFromDevice() {
meshService?.let { service ->
// Update our nodeinfos based on data from the device
val nodes = service.nodes.associateBy { it.user?.id!! }
nodeDB.setNodes(nodes)
try {
// Pull down our real node ID - This must be done AFTER reading the nodedb because we need the DB to find our nodeinof object
nodeDB.setMyId(service.myId)
} catch (ex: Exception) {
warn("Ignoring failure to get myId, service is probably just uninited... ${ex.message}")
}
}
}
private inline fun updateLoraConfig(crossinline body: (Config.LoRaConfig) -> Config.LoRaConfig) {
2022-09-12 00:26:12 -03:00
val data = body(config.lora)
2022-10-11 16:27:36 -03:00
setConfig(config { lora = data })
2022-09-12 00:26:12 -03:00
}
// Set the radio config (also updates our saved copy in preferences)
2022-10-11 16:27:36 -03:00
fun setConfig(config: Config) {
meshService?.setConfig(config.toByteArray())
2021-02-27 11:44:05 +08:00
}
fun setRemoteConfig(destNum: Int, config: Config) {
meshService?.setRemoteConfig(destNum, config.toByteArray())
2022-11-22 22:01:37 -03:00
}
fun setModuleConfig(destNum: Int, config: ModuleConfig) {
meshService?.setModuleConfig(destNum, config.toByteArray())
2022-11-22 22:01:37 -03:00
}
2023-05-02 07:18:22 -03:00
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 {
2022-10-16 12:40:05 -03:00
role = when (i) {
0 -> ChannelProtos.Channel.Role.PRIMARY
in 1..new.lastIndex -> ChannelProtos.Channel.Role.SECONDARY
2022-10-16 12:40:05 -03:00
else -> ChannelProtos.Channel.Role.DISABLED
}
2022-10-11 16:27:36 -03:00
index = i
settings = new.getOrNull(i) ?: channelSettings { }
})
2022-10-11 16:27:36 -03:00
}
}.forEach { setRemoteChannel(destNum, it) }
if (destNum == myNodeNum) viewModelScope.launch {
radioConfigRepository.replaceAllSettings(new)
}
}
private fun updateChannels(
old: List<ChannelSettings>,
new: List<ChannelSettings>
) {
updateChannels(myNodeNum ?: return, old, new)
}
/**
* Convert the [channels] array to and from [ChannelSet]
*/
private var _channelSet: AppOnlyProtos.ChannelSet
get() = channels.value.protobuf
set(value) {
updateChannels(channelSet.settingsList, value.settingsList)
2022-10-11 16:27:36 -03:00
val newConfig = config { lora = value.loraConfig }
if (config.lora != newConfig.lora) setConfig(newConfig)
}
val channelSet get() = _channelSet
2021-02-27 11:44:05 +08:00
/// Set the radio config (also updates our saved copy in preferences)
2022-10-11 16:27:36 -03:00
fun setChannels(channelSet: ChannelSet) {
this._channelSet = channelSet.protobuf
}
private fun setRemoteChannel(destNum: Int, channel: ChannelProtos.Channel) {
try {
meshService?.setRemoteChannel(destNum, channel.toByteArray())
} catch (ex: RemoteException) {
errormsg("Can't set channel on radio ${ex.message}")
}
2023-04-29 07:14:30 -03:00
}
2022-06-22 22:07:55 -03:00
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean("provide-location", false)) {
2021-06-23 12:41:44 -07:00
override fun setValue(value: Boolean) {
super.setValue(value)
2022-04-22 17:22:06 -03:00
preferences.edit {
2022-06-22 22:07:55 -03:00
this.putBoolean("provide-location", value)
2021-06-23 12:41:44 -07:00
}
}
}
2023-05-26 16:18:02 -03:00
fun setOwner(user: User) {
setRemoteOwner(myNodeNum ?: return, user)
2020-02-18 10:40:02 -08:00
}
2022-02-03 18:15:06 -08:00
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}")
}
}
2022-10-16 12:36:21 -03:00
val adminChannelIndex: Int
get() = channelSet.settingsList.map { it.name.lowercase() }.indexOf("admin")
2022-10-10 18:09:20 -03:00
2022-02-03 18:15:06 -08:00
/**
* Write the persisted packet data out to a CSV file in the specified location.
*/
fun saveMessagesCSV(file_uri: Uri) {
viewModelScope.launch(Dispatchers.Main) {
// Extract distances to this device from position messages and put (node,SNR,distance) in
// the file_uri
2023-02-03 19:07:15 -03:00
val myNodeNum = myNodeNum ?: return@launch
2022-02-03 18:15:06 -08:00
// Capture the current node value while we're still on main thread
val nodes = nodeDB.nodes.value ?: emptyMap()
val positionToPos: (MeshProtos.Position?) -> Position? = { meshPosition ->
meshPosition?.let { Position(it) }.takeIf {
it?.isValid() == true
}
}
2022-02-03 18:15:06 -08:00
writeToUri(file_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?>()
2022-02-03 18:15:06 -08:00
writer.appendLine("date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload")
// Packets are ordered by time, we keep most recent position of
// our device in localNodePosition.
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
2022-09-13 22:49:38 -03:00
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 {
nodePositions[nodeInfo.num] = nodeInfo.position
}
}
packet.meshPacket?.let { proto ->
// If the packet contains position data then use it to update, if valid
2022-02-03 18:15:06 -08:00
packet.position?.let { position ->
positionToPos.invoke(position)?.let {
nodePositions[proto.from] = position
}
}
// Filter out of our results any packet that doesn't report SNR. This
// is primarily ADMIN_APP.
2022-10-26 17:03:51 -03:00
if (proto.rxSnr != 0.0f) {
val rxDateTime = dateFormat.format(packet.received_date)
val rxFrom = proto.from.toUInt()
val senderName = nodesById[proto.from]?.user?.longName ?: ""
// sender lat & long
val senderPosition = nodePositions[proto.from]
val senderPos = positionToPos.invoke(senderPosition)
val senderLat = senderPos?.latitude ?: ""
val senderLong = senderPos?.longitude ?: ""
// rx lat, long, and elevation
val rxPosition = nodePositions[myNodeNum]
val rxPos = positionToPos.invoke(rxPosition)
val rxLat = rxPos?.latitude ?: ""
val rxLong = rxPos?.longitude ?: ""
val rxAlt = rxPos?.altitude ?: ""
val rxSnr = "%f".format(proto.rxSnr)
// Calculate the distance if both positions are valid
val dist = if (senderPos == null || rxPos == null) {
""
2022-02-03 18:15:06 -08:00
} else {
positionToMeter(
rxPosition!!, // Use rxPosition but only if rxPos was valid
senderPosition!! // Use senderPosition but only if senderPos was valid
).roundToInt().toString()
}
val hopLimit = proto.hopLimit
val payload = when {
proto.decoded.portnumValue != Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> "<${proto.decoded.portnum}>"
proto.hasDecoded() -> "\"" + proto.decoded.payload.toStringUtf8()
.replace("\"", "\\\"") + "\""
proto.hasEncrypted() -> "${proto.encrypted.size()} encrypted bytes"
else -> ""
2022-02-03 18:15:06 -08:00
}
// date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload
writer.appendLine("$rxDateTime,$rxFrom,$senderName,$senderLat,$senderLong,$rxLat,$rxLong,$rxAlt,$rxSnr,$dist,$hopLimit,$payload")
2022-02-03 18:15:06 -08:00
}
}
}
}
}
}
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) {
withContext(Dispatchers.IO) {
2022-02-13 08:09:26 -03:00
try {
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
FileWriter(parcelFileDescriptor.fileDescriptor).use { fileWriter ->
BufferedWriter(fileWriter).use { writer ->
block.invoke(writer)
}
2022-02-03 18:15:06 -08:00
}
}
2022-02-13 08:09:26 -03:00
} catch (ex: FileNotFoundException) {
errormsg("Can't write file error: ${ex.message}")
2022-02-03 18:15:06 -08:00
}
}
}
2023-05-02 07:18:22 -03:00
private val _deviceProfile = MutableStateFlow<DeviceProfile?>(null)
val deviceProfile: StateFlow<DeviceProfile?> = _deviceProfile
fun setDeviceProfile(deviceProfile: DeviceProfile?) {
_deviceProfile.value = deviceProfile
}
fun importProfile(file_uri: Uri) = viewModelScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
var inputStream: InputStream? = null
try {
inputStream = app.contentResolver.openInputStream(file_uri)
val bytes = inputStream?.readBytes()
val protobuf = DeviceProfile.parseFrom(bytes)
_deviceProfile.value = protobuf
} catch (ex: Exception) {
errormsg("Failed to import radio configs: ${ex.message}")
} finally {
inputStream?.close()
}
}
}
fun exportProfile(file_uri: Uri) = viewModelScope.launch {
val profile = deviceProfile.value ?: return@launch
writeToUri(file_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)
}
}
} catch (ex: FileNotFoundException) {
errormsg("Can't write file error: ${ex.message}")
}
}
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
)
2023-05-26 16:18:02 -03:00
setOwner(user.toProto())
2023-05-02 07:18:22 -03:00
}
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 })
}
// meshService?.commitEditSettings()
}
2022-09-22 08:35:33 -04:00
fun parseUrl(url: String, map: MapView) {
viewModelScope.launch(Dispatchers.IO) {
parseIt(url, map)
}
}
// For Future Use
// model.parseUrl(
// "https://www.google.com/maps/d/kml?forcekml=1&mid=1FmqWhZG3PG3dY92x9yf2RlREcK7kMZs&lid=-ivSjBCePsM",
// map
// )
private fun parseIt(url: String, map: MapView) {
val kmlDoc = KmlDocument()
try {
kmlDoc.parseKMLUrl(url)
val kmlOverlay = kmlDoc.mKmlRoot.buildOverlay(map, null, null, kmlDoc) as FolderOverlay
kmlDoc.mKmlRoot.mItems
kmlDoc.mKmlRoot.mName
map.overlayManager.overlays().add(kmlOverlay)
} catch (ex: Exception) {
debug("Failed to download .kml $ex")
}
}
2022-08-10 17:29:17 +01:00
fun addQuickChatAction(name: String, value: String, mode: QuickChatAction.Mode) {
2022-08-11 16:43:26 +01:00
viewModelScope.launch(Dispatchers.Main) {
val action = QuickChatAction(0, name, value, mode, _quickChatActions.value.size)
2022-08-11 16:43:26 +01:00
quickChatActionRepository.insert(action)
}
}
fun deleteQuickChatAction(action: QuickChatAction) {
viewModelScope.launch(Dispatchers.Main) {
quickChatActionRepository.delete(action)
2022-08-11 16:43:26 +01:00
}
}
fun updateQuickChatAction(
action: QuickChatAction,
name: String?,
message: String?,
mode: QuickChatAction.Mode?
) {
viewModelScope.launch(Dispatchers.Main) {
val newAction = QuickChatAction(
action.uuid,
name ?: action.name,
message ?: action.message,
mode ?: action.mode,
action.position
2022-08-11 16:43:26 +01:00
)
quickChatActionRepository.update(newAction)
}
2022-08-10 17:29:17 +01:00
}
fun updateActionPositions(actions: List<QuickChatAction>) {
viewModelScope.launch(Dispatchers.Main) {
2022-08-16 12:25:40 +01:00
for (position in actions.indices) {
quickChatActionRepository.setItemPosition(actions[position].uuid, position)
}
}
}
2020-02-17 13:34:52 -08:00
}