Merge pull request #365 from mcumings/fix-340

Fixes #340 Improve CSV file export
This commit is contained in:
Andre Kirchhoff 2022-02-04 00:16:18 -03:00 committed by GitHub
commit a0d00a4287
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 125 additions and 61 deletions

View file

@ -43,7 +43,6 @@ import com.geeksville.android.Logging
import com.geeksville.android.ServiceClient import com.geeksville.android.ServiceClient
import com.geeksville.concurrent.handledLaunch import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.android.* import com.geeksville.mesh.android.*
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.databinding.ActivityMainBinding import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.ChannelSet import com.geeksville.mesh.model.ChannelSet
import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.DeviceVersion
@ -62,13 +61,14 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.vorlonsoft.android.rate.AppRate import com.vorlonsoft.android.rate.AppRate
import com.vorlonsoft.android.rate.StoreType import com.vorlonsoft.android.rate.StoreType
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import java.io.FileOutputStream import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import java.nio.charset.Charset import java.nio.charset.Charset
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.roundToInt
/* /*
@ -655,20 +655,7 @@ class MainActivity : AppCompatActivity(), Logging,
} }
CREATE_CSV_FILE -> { CREATE_CSV_FILE -> {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
data?.data?.let { file_uri -> data?.data?.let { file_uri -> model.saveMessagesCSV(file_uri) }
// model.allPackets is a result of a query, so we need to use observer for
// the query to materialize
model.allPackets.observe(this, { packets ->
if (packets != null) {
// no need for observer once got non-null list
model.allPackets.removeObservers(this)
// execute on the default thread pool to not block the main thread
CoroutineScope(Dispatchers.Default + Job()).handledLaunch {
saveMessagesCSV(file_uri, packets)
}
}
})
}
} }
} }
} }
@ -1190,40 +1177,6 @@ class MainActivity : AppCompatActivity(), Logging,
} }
} }
private fun saveMessagesCSV(file_uri: Uri, packets: List<Packet>) {
// Extract distances to this device from position messages and put (node,SNR,distance) in
// the file_uri
val myNodeNum = model.myNodeInfo.value?.myNodeNum ?: return
applicationContext.contentResolver.openFileDescriptor(file_uri, "w")?.use {
FileOutputStream(it.fileDescriptor).use { fs ->
// Write header
fs.write(("from,rssi,snr,time,dist\n").toByteArray())
// Packets are ordered by time, we keep most recent position of
// our device in my_position.
var my_position: MeshProtos.Position? = null
packets.forEach {
it.proto?.let { packet_proto ->
it.position?.let { position ->
if (packet_proto.from == myNodeNum) {
my_position = position
} else if (my_position != null) {
val dist = positionToMeter(my_position!!, position).roundToInt()
fs.write(
"%x,%d,%f,%d,%d\n".format(
packet_proto.from, packet_proto.rxRssi,
packet_proto.rxSnr, packet_proto.rxTime, dist
).toByteArray()
)
}
}
}
}
}
}
}
/// Theme functions /// Theme functions
private fun chooseThemeDialog() { private fun chooseThemeDialog() {

View file

@ -71,6 +71,13 @@ data class Position(
/// @return bearing to the other position in degrees /// @return bearing to the other position in degrees
fun bearing(o: Position) = bearing(latitude, longitude, o.latitude, o.longitude) fun bearing(o: Position) = bearing(latitude, longitude, o.latitude, o.longitude)
// If GPS gives a crap position don't crash our app
fun isValid(): Boolean {
return (latitude <= 90.0 && latitude >= -90) &&
latitude != 0.0 &&
longitude != 0.0
}
override fun toString(): String { override fun toString(): String {
return "Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=${time}, batteryPctLevel=${batteryPctLevel})" return "Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=${time}, batteryPctLevel=${batteryPctLevel})"
} }
@ -112,11 +119,7 @@ data class NodeInfo(
/// return the position if it is valid, else null /// return the position if it is valid, else null
val validPosition: Position? val validPosition: Position?
get() { get() {
return position?.takeIf { return position?.takeIf { it.isValid() }
(it.latitude <= 90.0 && it.latitude >= -90) && // If GPS gives a crap position don't crash our app
it.latitude != 0.0 &&
it.longitude != 0.0
}
} }
/// @return distance in meters to some other node (or null if unknown) /// @return distance in meters to some other node (or null if unknown)

View file

@ -3,9 +3,11 @@ package com.geeksville.mesh.database
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.geeksville.mesh.database.dao.PacketDao import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.Packet
import kotlinx.coroutines.flow.Flow
class PacketRepository(private val packetDao : PacketDao) { class PacketRepository(private val packetDao : PacketDao) {
val allPackets : LiveData<List<Packet>> = packetDao.getAllPacket(500) val allPackets : LiveData<List<Packet>> = packetDao.getAllPacket(MAX_ITEMS)
val allPacketsInReceiveOrder : Flow<List<Packet>> = packetDao.getAllPacketsInReceiveOrder(MAX_ITEMS)
suspend fun insert(packet: Packet) { suspend fun insert(packet: Packet) {
packetDao.insert(packet) packetDao.insert(packet)
@ -14,4 +16,9 @@ class PacketRepository(private val packetDao : PacketDao) {
suspend fun deleteAll() { suspend fun deleteAll() {
packetDao.deleteAll() packetDao.deleteAll()
} }
companion object {
private const val MAX_ITEMS = 500
}
} }

View file

@ -5,6 +5,7 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.Packet
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface PacketDao { interface PacketDao {
@ -12,6 +13,9 @@ interface PacketDao {
@Query("Select * from packet order by received_date desc limit 0,:maxItem") @Query("Select * from packet order by received_date desc limit 0,:maxItem")
fun getAllPacket(maxItem: Int): LiveData<List<Packet>> fun getAllPacket(maxItem: Int): LiveData<List<Packet>>
@Query("Select * from packet order by received_date asc limit 0,:maxItem")
fun getAllPacketsInReceiveOrder(maxItem: Int): Flow<List<Packet>>
@Insert @Insert
fun insert(packet: Packet) fun insert(packet: Packet)

View file

@ -12,15 +12,21 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.geeksville.android.Logging import com.geeksville.android.Logging
import com.geeksville.mesh.IMeshService import com.geeksville.mesh.*
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.RadioConfigProtos
import com.geeksville.mesh.database.MeshtasticDatabase import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.positionToMeter
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedWriter
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.*
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 /// 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 /// that user. If the original name is only one word, strip vowels from the original name and if the result is
@ -257,5 +263,96 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
errormsg("Can't set username on device, is device offline? ${ex.message}") errormsg("Can't set username on device, is device offline? ${ex.message}")
} }
} }
/**
* 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
val myNodeNum = myNodeInfo.value?.myNodeNum ?: return@launch
// Capture the current node value while we're still on main thread
val nodes = nodeDB.nodes.value ?: emptyMap()
writeToUri(file_uri) { writer ->
// Create a map of nodes keyed by their ID
val nodesById = nodes.values.associateBy { it.num }
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.
var localNodePosition: MeshProtos.Position? = null
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
repository.allPacketsInReceiveOrder.first().forEach { packet ->
packet.proto?.let { proto ->
packet.position?.let { position ->
if (proto.from == myNodeNum) {
localNodePosition = position
} else {
val rxDateTime = dateFormat.format(packet.received_date)
val rxFrom = proto.from.toUInt()
val senderName = nodesById[proto.from]?.user?.longName ?: ""
// sender lat & long
val senderPos = packet.position
?.let { p -> Position(p) }
?.takeIf { p -> p.isValid() }
val senderLat = senderPos?.latitude ?: ""
val senderLong = senderPos?.longitude ?: ""
// rx lat, long, and elevation
val rxPos = localNodePosition
?.let { p -> Position(p) }
?.takeIf { p -> p.isValid() }
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) {
""
} else {
positionToMeter(
localNodePosition!!,
position
).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 -> ""
}
// 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")
}
}
}
}
}
}
}
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) {
withContext(Dispatchers.IO) {
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
FileWriter(parcelFileDescriptor.fileDescriptor).use { fileWriter ->
BufferedWriter(fileWriter).use { writer ->
block.invoke(writer)
}
}
}
}
}
} }