mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-16 12:59:00 -04:00
Merge pull request #396 from meshtastic/refactor-protos
add telemetry.protos
This commit is contained in:
@@ -19,8 +19,7 @@ data class MeshUser(
|
||||
val longName: String,
|
||||
val shortName: String,
|
||||
val hwModel: MeshProtos.HardwareModel
|
||||
) :
|
||||
Parcelable {
|
||||
) : Parcelable {
|
||||
|
||||
override fun toString(): String {
|
||||
return "MeshUser(id=${id.anonymize}, longName=${longName.anonymize}, shortName=${shortName.anonymize}, hwModel=${hwModelString})"
|
||||
@@ -31,10 +30,8 @@ data class MeshUser(
|
||||
* */
|
||||
val hwModelString: String?
|
||||
get() =
|
||||
if (hwModel == MeshProtos.HardwareModel.UNSET)
|
||||
null
|
||||
else
|
||||
hwModel.name.replace('_', '-').replace('p', '.').toLowerCase()
|
||||
if (hwModel == MeshProtos.HardwareModel.UNSET) null
|
||||
else hwModel.name.replace('_', '-').replace('p', '.').lowercase()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -43,8 +40,7 @@ data class Position(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val altitude: Int,
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val batteryPctLevel: Int = 0
|
||||
val time: Int = currentTime() // default to current time in secs (NOT MILLISECONDS!)
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
/// Convert to a double representation of degrees
|
||||
@@ -61,8 +57,7 @@ data class Position(
|
||||
degD(p.latitudeI),
|
||||
degD(p.longitudeI),
|
||||
p.altitude,
|
||||
if (p.time != 0) p.time else defaultTime,
|
||||
p.batteryLevel
|
||||
if (p.time != 0) p.time else defaultTime
|
||||
)
|
||||
|
||||
/// @return distance in meters to some other node (or null if unknown)
|
||||
@@ -79,7 +74,36 @@ data class Position(
|
||||
}
|
||||
|
||||
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})"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class Telemetry(
|
||||
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
|
||||
val batteryLevel: Int = 0,
|
||||
val voltage: Float,
|
||||
val channelUtilization: Float,
|
||||
val airUtilTx: Float
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
}
|
||||
|
||||
/** Create our model object from a protobuf.
|
||||
*/
|
||||
constructor(p: TelemetryProtos.Telemetry, defaultTime: Int = currentTime()) : this(
|
||||
if (p.time != 0) p.time else defaultTime,
|
||||
p.deviceMetrics.batteryLevel,
|
||||
p.deviceMetrics.voltage,
|
||||
p.deviceMetrics.channelUtilization,
|
||||
p.deviceMetrics.airUtilTx
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
return "Telemetry(time=${time}, batteryLevel=${batteryLevel}, voltage=${voltage}, channelUtilization=${channelUtilization}, airUtilTx=${airUtilTx})"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,14 +116,11 @@ data class NodeInfo(
|
||||
var position: Position? = null,
|
||||
var snr: Float = Float.MAX_VALUE,
|
||||
var rssi: Int = Int.MAX_VALUE,
|
||||
var lastHeard: Int = 0 // the last time we've seen this node in secs since 1970
|
||||
var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
|
||||
var telemetry: Telemetry? = null
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* Return the last time we've seen this node in secs since 1970
|
||||
*/
|
||||
|
||||
val batteryPctLevel get() = position?.batteryPctLevel
|
||||
val batteryPctLevel get() = telemetry?.batteryLevel
|
||||
|
||||
/**
|
||||
* true if the device was heard from recently
|
||||
|
||||
@@ -10,15 +10,9 @@ import com.geeksville.mesh.Position
|
||||
/// NodeDB lives inside the UIViewModel, but it needs a backpointer to reach the service
|
||||
class NodeDB(private val ui: UIViewModel) {
|
||||
private val testPositions = arrayOf(
|
||||
Position(32.776665, -96.796989, 35, 123, 40), // dallas
|
||||
Position(32.960758, -96.733521, 35, 456, 50), // richardson
|
||||
Position(
|
||||
32.912901,
|
||||
-96.781776,
|
||||
35,
|
||||
789,
|
||||
60
|
||||
) // north dallas
|
||||
Position(32.776665, -96.796989, 35, 123), // dallas
|
||||
Position(32.960758, -96.733521, 35, 456), // richardson
|
||||
Position(32.912901, -96.781776, 35, 789) // north dallas
|
||||
)
|
||||
|
||||
val testNodeNoPosition = NodeInfo(
|
||||
|
||||
@@ -780,6 +780,14 @@ class MeshService : Service(), Logging {
|
||||
handleReceivedUser(packet.from, u)
|
||||
}
|
||||
|
||||
// Handle new telemetry info
|
||||
Portnums.PortNum.TELEMETRY_APP_VALUE -> {
|
||||
var u = TelemetryProtos.Telemetry.parseFrom(data.payload)
|
||||
if (u.time == 0 && packet.rxTime != 0)
|
||||
u = u.toBuilder().setTime(packet.rxTime).build()
|
||||
handleReceivedTelemetry(packet.from, u, dataPacket.time)
|
||||
}
|
||||
|
||||
// Handle new style routing info
|
||||
Portnums.PortNum.ROUTING_APP_VALUE -> {
|
||||
shouldBroadcast =
|
||||
@@ -892,6 +900,17 @@ class MeshService : Service(), Logging {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update our DB of users based on someone sending out a User subpacket
|
||||
private fun handleReceivedTelemetry(
|
||||
fromNum: Int,
|
||||
p: TelemetryProtos.Telemetry,
|
||||
defaultTime: Long = System.currentTimeMillis()
|
||||
) {
|
||||
updateNodeInfo(fromNum) {
|
||||
it.telemetry = Telemetry(p, (defaultTime / 1000L).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
/// If packets arrive before we have our node DB, we delay parsing them until the DB is ready
|
||||
// private val earlyReceivedPackets = mutableListOf<MeshPacket>()
|
||||
|
||||
@@ -1269,12 +1288,18 @@ class MeshService : Service(), Logging {
|
||||
it.position = Position(info.position)
|
||||
}
|
||||
|
||||
if (info.hasTelemetry()) {
|
||||
// For the local node, it might not be able to update its times because it doesn't have a valid GPS reading yet
|
||||
// so if the info is for _our_ node we always assume time is current
|
||||
it.telemetry = Telemetry(info.telemetry)
|
||||
}
|
||||
|
||||
it.lastHeard = info.lastHeard
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNodeInfo(info: MeshProtos.NodeInfo) {
|
||||
debug("Received nodeinfo num=${info.num}, hasUser=${info.hasUser()}, hasPosition=${info.hasPosition()}")
|
||||
debug("Received nodeinfo num=${info.num}, hasUser=${info.hasUser()}, hasPosition=${info.hasPosition()}, hasTelemetry=${info.hasTelemetry()}")
|
||||
|
||||
val packetToSave = Packet(
|
||||
UUID.randomUUID().toString(),
|
||||
|
||||
@@ -159,7 +159,6 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi
|
||||
position = MeshProtos.Position.newBuilder().apply {
|
||||
latitudeI = Position.degI(lat)
|
||||
longitudeI = Position.degI(lon)
|
||||
batteryLevel = 42
|
||||
altitude = 35
|
||||
time = (System.currentTimeMillis() / 1000).toInt()
|
||||
}.build()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -20,7 +19,6 @@ import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.util.formatAgo
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.net.URLEncoder
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class UsersFragment : ScreenFragment("Users"), Logging {
|
||||
@@ -123,7 +121,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
||||
"utf-8"
|
||||
)
|
||||
}'>${coords}</a>"
|
||||
holder.coordsView.text = HtmlCompat.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
|
||||
holder.coordsView.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
holder.coordsView.movementMethod = LinkMovementMethod.getInstance()
|
||||
holder.coordsView.visibility = View.VISIBLE
|
||||
} else {
|
||||
@@ -138,32 +136,30 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
||||
} else {
|
||||
holder.distanceView.visibility = View.INVISIBLE
|
||||
}
|
||||
renderBattery(n.batteryPctLevel, holder)
|
||||
renderBattery(n.batteryPctLevel, n.telemetry?.voltage, holder)
|
||||
|
||||
holder.lastTime.text = formatAgo(n.lastHeard)
|
||||
|
||||
if ((n.num == ourNodeInfo?.num) || (n.snr > 100f)) {
|
||||
holder.signalView.visibility = View.INVISIBLE
|
||||
} else {
|
||||
val text = if (n.rssi < 0) {
|
||||
"rssi:${n.rssi} snr:${n.snr.roundToInt()}"
|
||||
} else {
|
||||
// Older devices do not send rssi. Remove this branch once upgraded past 1.2.1
|
||||
"snr:${n.snr.roundToInt()}"
|
||||
}
|
||||
holder.signalView.text = text
|
||||
holder.signalView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (n.num == ourNodeInfo?.num) {
|
||||
val info = model.myNodeInfo.value
|
||||
if (info != null) {
|
||||
val channelUtilizationText = String.format("%.1f", info.channelUtilization)
|
||||
val airUtilTxText = String.format("%.1f", info.airUtilTx)
|
||||
val combinedText = "ChUtil $channelUtilizationText% AirUtilTX $airUtilTxText%"
|
||||
holder.signalView.text = combinedText
|
||||
val text =
|
||||
String.format(
|
||||
"ChUtil %.1f%% AirUtilTX %.1f%%",
|
||||
n.telemetry?.channelUtilization ?: info.channelUtilization,
|
||||
n.telemetry?.airUtilTx ?: info.airUtilTx
|
||||
)
|
||||
holder.signalView.text = text
|
||||
holder.signalView.visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
if ((n.snr < 100f) && (n.rssi < 0)) {
|
||||
val text = String.format("rssi:%d snr:%.1f", n.rssi, n.snr)
|
||||
holder.signalView.text = text
|
||||
holder.signalView.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.signalView.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,11 +174,15 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
||||
|
||||
private fun renderBattery(
|
||||
battery: Int?,
|
||||
voltage: Float?,
|
||||
holder: ViewHolder
|
||||
) {
|
||||
|
||||
val (image, text) = when (battery) {
|
||||
in 1..100 -> Pair(R.drawable.ic_battery_full_24, "$battery%")
|
||||
in 1..100 -> Pair(
|
||||
R.drawable.ic_battery_full_24,
|
||||
String.format("%d%% %.2fV", battery, voltage ?: 0)
|
||||
)
|
||||
0 -> Pair(R.drawable.ic_power_plug_24, "")
|
||||
else -> Pair(R.drawable.ic_battery_full_24, "?")
|
||||
}
|
||||
@@ -199,7 +199,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
_binding = NodelistFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
@@ -210,53 +210,8 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
||||
binding.nodeListView.adapter = nodesAdapter
|
||||
binding.nodeListView.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
model.nodeDB.nodes.observe(viewLifecycleOwner, {
|
||||
model.nodeDB.nodes.observe(viewLifecycleOwner) {
|
||||
nodesAdapter.onNodesChanged(it.values)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
|
||||
if (false) { // hide the firmware update button for now, it is kinda ugly and users don't need it yet
|
||||
/// Create a software update button
|
||||
val context = ContextAmbient.current
|
||||
RadioInterfaceService.getBondedDeviceAddress(context)?.let { macAddress ->
|
||||
Button(
|
||||
onClick = {
|
||||
SoftwareUpdateService.enqueueWork(
|
||||
context,
|
||||
SoftwareUpdateService.startUpdateIntent(macAddress)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(text = "Update firmware")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* FIXME - doens't work yet - probably because I'm not using release keys
|
||||
// If account is null, then show the signin button, otherwise
|
||||
val context = ambient(ContextAmbient)
|
||||
val account = GoogleSignIn.getLastSignedInAccount(context)
|
||||
if (account != null)
|
||||
Text("We have an account")
|
||||
else {
|
||||
Text("No account yet")
|
||||
if (context is Activity) {
|
||||
Button("Google sign-in", onClick = {
|
||||
val signInIntent: Intent = UIState.googleSignInClient.signInIntent
|
||||
context.startActivityForResult(signInIntent, MainActivity.RC_SIGN_IN)
|
||||
})
|
||||
}
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user