Merge pull request #260 from vfurman-gh/master

Save messages in CSV file
pull/259/head^2
Kevin Hester 2021-03-19 14:57:42 +08:00 zatwierdzone przez GitHub
commit dc96565ff1
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
7 zmienionych plików z 126 dodań i 27 usunięć

Wyświetl plik

@ -20,9 +20,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.RemoteException import android.os.RemoteException
import android.text.SpannableString
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.text.util.Linkify
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent import android.view.MotionEvent
@ -43,8 +41,8 @@ import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging 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.database.entity.Packet
import com.geeksville.mesh.databinding.ActivityMainBinding import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.ChannelSet import com.geeksville.mesh.model.ChannelSet
import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
@ -66,9 +64,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import java.io.FileOutputStream
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 kotlin.math.roundToInt
/* /*
@ -132,6 +132,7 @@ class MainActivity : AppCompatActivity(), Logging,
const val RC_SIGN_IN = 12 // google signin completed const val RC_SIGN_IN = 12 // google signin completed
const val RC_SELECT_DEVICE = const val RC_SELECT_DEVICE =
13 // seems to be hardwired in CompanionDeviceManager to add 65536 13 // seems to be hardwired in CompanionDeviceManager to add 65536
const val CREATE_CSV_FILE = 14
} }
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@ -555,6 +556,18 @@ class MainActivity : AppCompatActivity(), Logging,
else -> else ->
warn("BLE device select intent failed") warn("BLE device select intent failed")
} }
CREATE_CSV_FILE -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.let { file_uri ->
model.allPackets.observe(this, { packets ->
if (packets != null) {
saveMessagesCSV(file_uri, packets)
model.allPackets.removeObservers(this)
}
})
}
}
}
} }
} }
@ -659,7 +672,7 @@ class MainActivity : AppCompatActivity(), Logging,
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0") val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
val minVer = DeviceVersion("1.2.0") val minVer = DeviceVersion("1.2.0")
if(curVer < minVer) if (curVer < minVer)
showAlert(R.string.firmware_too_old, R.string.firmware_old) showAlert(R.string.firmware_too_old, R.string.firmware_old)
else { else {
// If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here // If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here
@ -869,8 +882,7 @@ class MainActivity : AppCompatActivity(), Logging,
errormsg("Device error during init ${ex.message}") errormsg("Device error during init ${ex.message}")
model.isConnected.value = model.isConnected.value =
MeshService.ConnectionState.valueOf(service.connectionState()) MeshService.ConnectionState.valueOf(service.connectionState())
} } finally {
finally {
connectionJob = null connectionJob = null
} }
@ -1029,6 +1041,15 @@ class MainActivity : AppCompatActivity(), Logging,
fragmentTransaction.commit() fragmentTransaction.commit()
return true return true
} }
R.id.save_messages_csv -> {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv"
putExtra(Intent.EXTRA_TITLE, "messages.csv")
}
startActivityForResult(intent, CREATE_CSV_FILE)
return true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
@ -1042,4 +1063,38 @@ class MainActivity : AppCompatActivity(), Logging,
errormsg("Can not find the version: ${e.message}") errormsg("Can not find the version: ${e.message}")
} }
} }
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,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: Int =
positionToMeter(my_position!!, position).roundToInt()
fs.write(
("${packet_proto.from.toUInt().toString(16)}," +
"${packet_proto.rxSnr},${packet_proto.rxTime},$dist\n")
.toByteArray()
)
}
}
}
}
}
}
}
} }

Wyświetl plik

@ -9,7 +9,7 @@ import com.geeksville.mesh.database.entity.Packet
@Dao @Dao
interface PacketDao { interface PacketDao {
@Query("Select * from packet order by rowid 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>>
@Insert @Insert

Wyświetl plik

@ -3,6 +3,10 @@ package com.geeksville.mesh.database.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.Portnums
import com.google.protobuf.TextFormat
import java.io.IOException
@Entity(tableName = "packet") @Entity(tableName = "packet")
@ -12,6 +16,25 @@ data class Packet(@PrimaryKey val uuid: String,
@ColumnInfo(name = "message") val raw_message: String @ColumnInfo(name = "message") val raw_message: String
) { ) {
val proto: MeshProtos.MeshPacket?
get() {
if (message_type == "packet") {
val builder = MeshProtos.MeshPacket.newBuilder()
try {
TextFormat.getParser().merge(raw_message, builder)
return builder.build()
} catch (e: IOException) {
}
}
return null
}
val position: MeshProtos.Position?
get() {
return proto?.run {
if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) {
return MeshProtos.Position.parseFrom(decoded.payload)
}
return null
}
}
} }

Wyświetl plik

@ -163,15 +163,13 @@ class MeshService : Service(), Logging {
*/ */
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@UiThread @UiThread
private fun startLocationRequests() { private fun startLocationRequests(requestInterval: Long) {
// FIXME - currently we don't support location reading without google play // FIXME - currently we don't support location reading without google play
if (fusedLocationClient == null && isGooglePlayAvailable(this)) { if (fusedLocationClient == null && isGooglePlayAvailable(this)) {
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
val request = LocationRequest.create().apply { val request = LocationRequest.create().apply {
interval = interval = requestInterval
5 * 60 * 1000 // FIXME, do more like once every 5 mins while we are connected to our radio _and_ someone else is in the mesh
priority = LocationRequest.PRIORITY_HIGH_ACCURACY priority = LocationRequest.PRIORITY_HIGH_ACCURACY
} }
val builder = LocationSettingsRequest.Builder().addLocationRequest(request) val builder = LocationSettingsRequest.Builder().addLocationRequest(request)
@ -938,19 +936,28 @@ class MeshService : Service(), Logging {
private fun onNodeDBChanged() { private fun onNodeDBChanged() {
maybeUpdateServiceStatusNotification() maybeUpdateServiceStatusNotification()
// we don't ask for GPS locations from android if our device has a built in GPS serviceScope.handledLaunch(Dispatchers.Main) {
// Note: myNodeInfo can go away if we lose connections, so it might be null setupLocationRequest()
if (myNodeInfo?.hasGPS != true) { }
// If we have at least one other person in the mesh, send our GPS position otherwise stop listening to GPS }
serviceScope.handledLaunch(Dispatchers.Main) { private var locationRequestInterval: Long = 0;
if (numOnlineNodes >= 2) private fun setupLocationRequest () {
startLocationRequests() val desiredInterval: Long = if (myNodeInfo?.hasGPS == true) {
else 0L // no requests when device has GPS
stopLocationRequests() } else if (numOnlineNodes < 2) {
} 5 * 60 * 1000L // send infrequently, device needs these requests to set its clock
} else } else {
debug("Our radio has a built in GPS, so not reading GPS in phone") radioConfig?.preferences?.positionBroadcastSecs?.times( 1000L) ?: 5 * 60 * 1000L
}
debug("desired location request $desiredInterval, current $locationRequestInterval")
if (desiredInterval != locationRequestInterval) {
if (locationRequestInterval > 0) stopLocationRequests()
if (desiredInterval > 0) startLocationRequests(desiredInterval)
locationRequestInterval = desiredInterval
}
} }

Wyświetl plik

@ -1,5 +1,6 @@
package com.geeksville.mesh.ui package com.geeksville.mesh.ui
import com.geeksville.mesh.MeshProtos
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
@ -124,6 +125,14 @@ fun latLongToMeter(
return 6366000 * tt return 6366000 * tt
} }
// Same as above, but takes Mesh Position proto.
fun positionToMeter(a: MeshProtos.Position, b: MeshProtos.Position): Double {
return latLongToMeter(
a.latitudeI * 1e-7,
a.longitudeI * 1e-7,
b.latitudeI * 1e-7,
b.longitudeI * 1e-7)
}
/** /**
* Convert degrees/mins/secs to a single double * Convert degrees/mins/secs to a single double
* *
@ -186,4 +195,4 @@ fun bearing(
*/ */
fun radToBearing(rad: Double): Double { fun radToBearing(rad: Double): Double {
return (Math.toDegrees(rad) + 360) % 360 return (Math.toDegrees(rad) + 360) % 360
} }

Wyświetl plik

@ -20,6 +20,10 @@
android:id="@+id/advanced_settings" android:id="@+id/advanced_settings"
app:showAsAction="withText" app:showAsAction="withText"
android:title="@string/advanced_settings" /> android:title="@string/advanced_settings" />
<item
android:id="@+id/save_messages_csv"
app:showAsAction="withText"
android:title="@string/save_messages" />
<item <item
android:id="@+id/about" android:id="@+id/about"
android:title="@string/about" android:title="@string/about"

Wyświetl plik

@ -94,4 +94,5 @@
<string name="okay">Okay</string> <string name="okay">Okay</string>
<string name="must_set_region">You must set a region!</string> <string name="must_set_region">You must set a region!</string>
<string name="region">Region</string> <string name="region">Region</string>
<string name="save_messages">Save messages as csv...</string>
</resources> </resources>