kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
commit
dc96565ff1
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Ładowanie…
Reference in New Issue