Merge remote-tracking branch 'root/master' into dev1.2

# Conflicts:
#	app/src/main/java/com/geeksville/mesh/service/MeshService.kt
#	app/src/main/java/com/geeksville/mesh/service/MockInterface.kt
#	app/src/main/res/values/strings.xml
1.2-legacy
Kevin Hester 2021-03-19 15:09:33 +08:00
commit a18343e30c
11 zmienionych plików z 176 dodań i 55 usunięć

Wyświetl plik

@ -42,6 +42,7 @@ import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.android.ServiceClient
import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.ChannelSet
import com.geeksville.mesh.model.DeviceVersion
@ -64,9 +65,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import java.io.FileOutputStream
import java.nio.charset.Charset
import java.text.DateFormat
import java.util.*
import kotlin.math.roundToInt
/*
@ -130,6 +133,7 @@ class MainActivity : AppCompatActivity(), Logging,
const val RC_SIGN_IN = 12 // google signin completed
const val RC_SELECT_DEVICE =
13 // seems to be hardwired in CompanionDeviceManager to add 65536
const val CREATE_CSV_FILE = 14
}
private lateinit var binding: ActivityMainBinding
@ -549,6 +553,18 @@ class MainActivity : AppCompatActivity(), Logging,
else ->
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)
}
})
}
}
}
}
}
@ -1040,6 +1056,15 @@ class MainActivity : AppCompatActivity(), Logging,
fragmentTransaction.commit()
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)
}
}
@ -1053,4 +1078,38 @@ class MainActivity : AppCompatActivity(), Logging,
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
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>>
@Insert

Wyświetl plik

@ -3,6 +3,10 @@ package com.geeksville.mesh.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
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")
@ -12,6 +16,25 @@ data class Packet(@PrimaryKey val uuid: 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")
@UiThread
private fun startLocationRequests() {
private fun startLocationRequests(requestInterval: Long) {
// FIXME - currently we don't support location reading without google play
if (fusedLocationClient == null && isGooglePlayAvailable(this)) {
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
val request = LocationRequest.create().apply {
interval =
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
interval = requestInterval
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
val builder = LocationSettingsRequest.Builder().addLocationRequest(request)
@ -941,19 +939,28 @@ class MeshService : Service(), Logging {
private fun onNodeDBChanged() {
maybeUpdateServiceStatusNotification()
// we don't ask for GPS locations from android if our device has a built in GPS
// Note: myNodeInfo can go away if we lose connections, so it might be null
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) {
setupLocationRequest()
}
}
serviceScope.handledLaunch(Dispatchers.Main) {
if (numOnlineNodes >= 2)
startLocationRequests()
else
stopLocationRequests()
}
} else
debug("Our radio has a built in GPS, so not reading GPS in phone")
private var locationRequestInterval: Long = 0;
private fun setupLocationRequest () {
val desiredInterval: Long = if (myNodeInfo?.hasGPS == true) {
0L // no requests when device has GPS
} else if (numOnlineNodes < 2) {
5 * 60 * 1000L // send infrequently, device needs these requests to set its clock
} else {
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
import com.geeksville.mesh.MeshProtos
import kotlin.math.cos
import kotlin.math.sin
@ -124,6 +125,14 @@ fun latLongToMeter(
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
*
@ -186,4 +195,4 @@ fun bearing(
*/
fun radToBearing(rad: Double): Double {
return (Math.toDegrees(rad) + 360) % 360
}
}

Wyświetl plik

@ -13,6 +13,7 @@ import com.geeksville.android.Logging
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.util.formatAgo
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
@ -79,7 +80,7 @@ class MapFragment : ScreenFragment("Map"), Logging {
)
)
node.user?.let {
f.addStringProperty("name", it.longName)
f.addStringProperty("name", it.longName + " " + formatAgo(p.time))
}
f
}
@ -93,7 +94,8 @@ class MapFragment : ScreenFragment("Map"), Logging {
}
fun zoomToNodes(map: MapboxMap) {
val nodesWithPosition = model.nodeDB.nodes.value?.values?.filter { it.validPosition != null }
val nodesWithPosition =
model.nodeDB.nodes.value?.values?.filter { it.validPosition != null }
if (nodesWithPosition != null && nodesWithPosition.isNotEmpty()) {
val update = if (nodesWithPosition.size >= 2) {
// Multiple nodes, make them all fit on the map view
@ -158,7 +160,10 @@ class MapFragment : ScreenFragment("Map"), Logging {
if (view != null) { // it might have gone away by now
// val markerIcon = BitmapFactory.decodeResource(context.resources, R.drawable.ic_twotone_person_pin_24)
val markerIcon =
ContextCompat.getDrawable(requireActivity(), R.drawable.ic_twotone_person_pin_24)!!
ContextCompat.getDrawable(
requireActivity(),
R.drawable.ic_twotone_person_pin_24
)!!
map.setStyle(Style.OUTDOORS) { style ->
style.addSource(nodePositions)
@ -176,7 +181,7 @@ class MapFragment : ScreenFragment("Map"), Logging {
// Any times nodes change update our map
model.nodeDB.nodes.observe(viewLifecycleOwner, Observer { nodes ->
if(isViewVisible)
if (isViewVisible)
onNodesChanged(map, nodes.values)
})
zoomToNodes(map)

Wyświetl plik

@ -2,11 +2,13 @@ package com.geeksville.mesh.ui
import android.os.Bundle
import android.text.format.DateFormat
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
@ -17,13 +19,14 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.AdapterNodeLayoutBinding
import com.geeksville.mesh.databinding.NodelistFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import java.text.ParseException
import java.util.*
import com.geeksville.util.formatAgo
import java.net.URLEncoder
class UsersFragment : ScreenFragment("Users"), Logging {
private var _binding: NodelistFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
@ -34,6 +37,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
class ViewHolder(itemView: AdapterNodeLayoutBinding) : RecyclerView.ViewHolder(itemView.root) {
val nodeNameView = itemView.nodeNameView
val distanceView = itemView.distanceView
val coordsView = itemView.coordsView
val batteryPctView = itemView.batteryPercentageView
val lastTime = itemView.lastConnectionView
val powerIcon = itemView.batteryIcon
@ -104,8 +108,26 @@ class UsersFragment : ScreenFragment("Users"), Logging {
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val n = nodes[position]
val name = n.user?.longName ?: n.user?.id ?: "Unknown node"
holder.nodeNameView.text = name
holder.nodeNameView.text = n.user?.longName ?: n.user?.id ?: "Unknown node"
val pos = n.validPosition;
if (pos != null) {
val coords =
String.format("%.5f %.5f", pos.latitude, pos.longitude).replace(",", ".")
val html =
"<a href='geo:${pos.latitude},${pos.longitude}?z=17&label=${
URLEncoder.encode(
name,
"utf-8"
)
}'>${coords}</a>"
holder.coordsView.text = HtmlCompat.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
holder.coordsView.movementMethod = LinkMovementMethod.getInstance()
holder.coordsView.visibility = View.VISIBLE
} else {
holder.coordsView.visibility = View.INVISIBLE
}
val ourNodeInfo = model.nodeDB.ourNodeInfo
val distance = ourNodeInfo?.distanceStr(n)
@ -118,7 +140,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
renderBattery(n.batteryPctLevel, holder)
holder.lastTime.text = getLastTimeValue(n)
holder.lastTime.text = formatAgo(n.lastSeen);
}
private var nodes = arrayOf<NodeInfo>()
@ -150,30 +172,6 @@ class UsersFragment : ScreenFragment("Users"), Logging {
})
}
private fun getLastTimeValue(n: NodeInfo): String {
var lastTimeText = "?"
val currentTime = (System.currentTimeMillis()/1000).toInt()
val threeDaysLong = 3 * 60*60*24
//if the lastSeen is too old
if (n.lastSeen < (currentTime - threeDaysLong))
return lastTimeText
try {
val toLong: Long = n.lastSeen.toLong()
val long1000 = toLong * 1000L
val date = Date(long1000)
val timeFormat = DateFormat.getTimeFormat(context)
lastTimeText = timeFormat.format(date)
} catch (e: ParseException) {
//
}
return lastTimeText
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?

Wyświetl plik

@ -51,6 +51,20 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<TextView
android:id="@+id/coords_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="@string/sample_coords"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/distance_view"
app:layout_constraintTop_toBottomOf="@+id/imageView"
app:layout_constraintVertical_bias="0.0" />
<ImageView
android:id="@+id/batteryIcon"
android:layout_width="wrap_content"

Wyświetl plik

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

Wyświetl plik

@ -95,4 +95,6 @@
<string name="must_set_region">You must set a region!</string>
<string name="region">Region</string>
<string name="cant_change_no_radio">Couldn\'t change channel, because radio is not yet connected. Please try again.</string>
<string name="sample_coords">55.332244 34.442211</string>
<string name="save_messages">Save messages as csv...</string>
</resources>

@ -1 +1 @@
Subproject commit 99cf0da30fe41163a735ac291f3dd018a7d6295d
Subproject commit 158f6f2dd5dfe81833ed035d54045d7b34394e51