feat: show per-message SNR, RSSI and hop count (#2040)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
Łukasz Kosson 2025-06-06 22:41:25 +02:00 committed by GitHub
parent 639213145b
commit 9a371ee9cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 794 additions and 8 deletions

View file

@ -59,6 +59,9 @@ data class DataPacket(
var hopLimit: Int = 0,
var channel: Int = 0, // channel index
var wantAck: Boolean = true, // If true, the receiver should send an ack back
var hopStart: Int = 0,
var snr: Float = 0f,
var rssi: Int = 0,
) : Parcelable {
/**
@ -107,6 +110,9 @@ data class DataPacket(
null
}
val hopsAway: Int
get() = if (hopStart == 0 || hopLimit > hopStart) -1 else hopStart - hopLimit
// Autogenerated comparision, because we have a byte array
constructor(parcel: Parcel) : this(
@ -120,8 +126,12 @@ data class DataPacket(
parcel.readInt(),
parcel.readInt(),
parcel.readInt() == 1,
parcel.readInt(),
parcel.readFloat(),
parcel.readInt(),
)
@Suppress("CyclomaticComplexMethod")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -138,6 +148,9 @@ data class DataPacket(
if (status != other.status) return false
if (hopLimit != other.hopLimit) return false
if (wantAck != other.wantAck) return false
if (hopStart != other.hopStart) return false
if (snr != other.snr) return false
if (rssi != other.rssi) return false
return true
}
@ -153,6 +166,9 @@ data class DataPacket(
result = 31 * result + hopLimit
result = 31 * result + channel
result = 31 * result + wantAck.hashCode()
result = 31 * result + hopStart
result = 31 * result + snr.hashCode()
result = 31 * result + rssi
return result
}
@ -167,6 +183,9 @@ data class DataPacket(
parcel.writeInt(hopLimit)
parcel.writeInt(channel)
parcel.writeInt(if (wantAck) 1 else 0)
parcel.writeInt(hopStart)
parcel.writeFloat(snr)
parcel.writeInt(rssi)
}
override fun describeContents(): Int {
@ -185,6 +204,9 @@ data class DataPacket(
hopLimit = parcel.readInt()
channel = parcel.readInt()
wantAck = parcel.readInt() == 1
hopStart = parcel.readInt()
snr = parcel.readFloat()
rssi = parcel.readInt()
}
companion object CREATOR : Parcelable.Creator<DataPacket> {

View file

@ -70,8 +70,9 @@ import com.geeksville.mesh.database.entity.ReactionEntity
AutoMigration(from = 14, to = 15),
AutoMigration(from = 15, to = 16),
AutoMigration(from = 16, to = 17),
AutoMigration(from = 17, to = 18),
],
version = 17,
version = 18,
exportSchema = true,
)
@TypeConverters(Converters::class)

View file

@ -41,6 +41,9 @@ data class PacketEntity(
node = getNode(data.from),
text = data.text.orEmpty(),
time = getShortDateTime(data.time),
snr = snr,
rssi = rssi,
hopsAway = hopsAway,
read = read,
status = data.status,
routingError = routingError,
@ -70,6 +73,9 @@ data class Packet(
@ColumnInfo(name = "packet_id", defaultValue = "0") val packetId: Int = 0,
@ColumnInfo(name = "routing_error", defaultValue = "-1") var routingError: Int = -1,
@ColumnInfo(name = "reply_id", defaultValue = "0") val replyId: Int = 0,
@ColumnInfo(name = "snr", defaultValue = "0") val snr: Float = 0f,
@ColumnInfo(name = "rssi", defaultValue = "0") val rssi: Int = 0,
@ColumnInfo(name = "hopsAway", defaultValue = "-1") val hopsAway: Int = -1,
)
@Entity(tableName = "contact_settings")

View file

@ -56,6 +56,9 @@ data class Message(
val routingError: Int,
val packetId: Int,
val emojis: List<Reaction>,
val snr: Float,
val rssi: Int,
val hopsAway: Int,
) {
fun getStatusStringRes(): Pair<Int, Int> {
val title = if (routingError > 0) R.string.error else R.string.message_delivery_status

View file

@ -673,6 +673,9 @@ class MeshService : Service(), Logging {
hopLimit = packet.hopLimit,
channel = if (packet.pkiEncrypted) DataPacket.PKC_CHANNEL_INDEX else packet.channel,
wantAck = packet.wantAck,
hopStart = packet.hopStart,
snr = packet.rxSnr,
rssi = packet.rxRssi
)
}
}
@ -705,7 +708,10 @@ class MeshService : Service(), Logging {
packetRepository.get().insertReaction(reaction)
}
private fun rememberDataPacket(dataPacket: DataPacket, updateNotification: Boolean = true) {
private fun rememberDataPacket(
dataPacket: DataPacket,
updateNotification: Boolean = true,
) {
if (dataPacket.dataType !in rememberDataType) return
val fromLocal = dataPacket.from == DataPacket.ID_LOCAL
val toBroadcast = dataPacket.to == DataPacket.ID_BROADCAST
@ -722,7 +728,10 @@ class MeshService : Service(), Logging {
contact_key = contactKey,
received_time = System.currentTimeMillis(),
read = fromLocal,
data = dataPacket
data = dataPacket,
snr = dataPacket.snr,
rssi = dataPacket.rssi,
hopsAway = dataPacket.hopsAway
)
serviceScope.handledLaunch {
packetRepository.get().apply {

View file

@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.TextUnit
import com.geeksville.mesh.R
private const val SNR_GOOD_THRESHOLD = -7f
@ -132,7 +133,7 @@ fun LoraSignalIndicator(snr: Float, rssi: Int) {
}
@Composable
private fun Snr(snr: Float) {
fun Snr(snr: Float, fontSize: TextUnit = MaterialTheme.typography.labelLarge.fontSize) {
val color: Color = if (snr > SNR_GOOD_THRESHOLD) {
Quality.GOOD.color
} else if (snr > SNR_FAIR_THRESHOLD) {
@ -144,12 +145,12 @@ private fun Snr(snr: Float) {
Text(
text = "%s %.2fdB".format(stringResource(id = R.string.snr), snr),
color = color,
fontSize = MaterialTheme.typography.labelLarge.fontSize
fontSize = fontSize
)
}
@Composable
private fun Rssi(rssi: Int) {
fun Rssi(rssi: Int, fontSize: TextUnit = MaterialTheme.typography.labelLarge.fontSize) {
val color: Color = if (rssi > RSSI_GOOD_THRESHOLD) {
Quality.GOOD.color
} else if (rssi > RSSI_FAIR_THRESHOLD) {
@ -160,7 +161,7 @@ private fun Rssi(rssi: Int) {
Text(
text = "%s %ddBm".format(stringResource(id = R.string.rssi), rssi),
color = color,
fontSize = MaterialTheme.typography.labelLarge.fontSize
fontSize = fontSize
)
}

View file

@ -191,7 +191,15 @@ internal fun MessageList(
onAction = onNodeMenuAction,
onStatusClick = { showStatusDialog = msg },
onSendReaction = { onSendReaction(it, msg.packetId) },
isConnected = isConnected
isConnected = isConnected,
snr = msg.snr,
rssi = msg.rssi,
hopsAway = if (msg.hopsAway > 0) { "%s: %d".format(
stringResource(id = R.string.hops_away),
msg.hopsAway
) } else {
null
}
)
}
}

View file

@ -25,6 +25,7 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
@ -53,6 +54,8 @@ import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.ui.common.components.AutoLinkText
import com.geeksville.mesh.ui.common.components.Rssi
import com.geeksville.mesh.ui.common.components.Snr
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.node.components.NodeChip
@ -74,6 +77,9 @@ internal fun MessageItem(
onStatusClick: () -> Unit = {},
onSendReaction: (String) -> Unit = {},
isConnected: Boolean,
snr: Float,
rssi: Int,
hopsAway: String?,
) = Row(
modifier = modifier
.fillMaxWidth()
@ -141,6 +147,17 @@ internal fun MessageItem(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
if (!fromLocal) {
if (hopsAway == null) {
Snr(snr, fontSize = MaterialTheme.typography.bodySmall.fontSize)
Spacer(Modifier.weight(1f))
Rssi(rssi, fontSize = MaterialTheme.typography.bodySmall.fontSize)
} else { Text(
text = hopsAway,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
) }
Spacer(Modifier.weight(1f))
}
Text(
text = messageTime,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
@ -181,6 +198,9 @@ private fun MessageItemPreview() {
messageStatus = MessageStatus.DELIVERED,
selected = false,
isConnected = true,
snr = 20.5f,
rssi = 90,
hopsAway = null
)
}
}