From 9a371ee9cd56720ddc9a7c111120ae6b9518bd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosson?= Date: Fri, 6 Jun 2025 22:41:25 +0200 Subject: [PATCH] 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 --- .../18.json | 716 ++++++++++++++++++ .../java/com/geeksville/mesh/DataPacket.kt | 22 + .../mesh/database/MeshtasticDatabase.kt | 3 +- .../geeksville/mesh/database/entity/Packet.kt | 6 + .../java/com/geeksville/mesh/model/Message.kt | 3 + .../geeksville/mesh/service/MeshService.kt | 13 +- .../common/components/LoraSignalIndicator.kt | 9 +- .../geeksville/mesh/ui/message/MessageList.kt | 10 +- .../mesh/ui/message/components/MessageItem.kt | 20 + 9 files changed, 794 insertions(+), 8 deletions(-) create mode 100644 app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/18.json diff --git a/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/18.json b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/18.json new file mode 100644 index 000000000..935c48f52 --- /dev/null +++ b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/18.json @@ -0,0 +1,716 @@ +{ + "formatVersion": 1, + "database": { + "version": 18, + "identityHash": "ebaac561066a33f7018bd9c945a4e5ac", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `paxcounter` BLOB NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "environmentTelemetry", + "columnName": "environment_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "powerTelemetry", + "columnName": "power_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "paxcounter", + "columnName": "paxcounter", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + } + }, + { + "tableName": "packet", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `reply_id` INTEGER NOT NULL DEFAULT 0, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "port_num", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_time", + "columnName": "received_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_packet_myNodeNum", + "unique": false, + "columnNames": [ + "myNodeNum" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)" + }, + { + "name": "index_packet_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)" + }, + { + "name": "index_packet_contact_key", + "unique": false, + "columnNames": [ + "contact_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)" + } + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "replyId", + "columnName": "reply_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emoji", + "columnName": "emoji", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "reply_id", + "user_id", + "emoji" + ] + }, + "indices": [ + { + "name": "index_reactions_reply_id", + "unique": false, + "columnNames": [ + "reply_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`hwModel`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "hwModel" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ebaac561066a33f7018bd9c945a4e5ac')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/DataPacket.kt b/app/src/main/java/com/geeksville/mesh/DataPacket.kt index b855359bb..58329eaa2 100644 --- a/app/src/main/java/com/geeksville/mesh/DataPacket.kt +++ b/app/src/main/java/com/geeksville/mesh/DataPacket.kt @@ -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 { diff --git a/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt b/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt index 740f0711d..5e6525022 100644 --- a/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt +++ b/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt @@ -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) diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt index cdcbdddaa..d6acdd113 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt @@ -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") diff --git a/app/src/main/java/com/geeksville/mesh/model/Message.kt b/app/src/main/java/com/geeksville/mesh/model/Message.kt index f8d52981c..6bf699eb6 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Message.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Message.kt @@ -56,6 +56,9 @@ data class Message( val routingError: Int, val packetId: Int, val emojis: List, + val snr: Float, + val rssi: Int, + val hopsAway: Int, ) { fun getStatusStringRes(): Pair { val title = if (routingError > 0) R.string.error else R.string.message_delivery_status diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 39bb31540..d68c5a704 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -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 { diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/LoraSignalIndicator.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/LoraSignalIndicator.kt index ce8a3a0e5..efeebd655 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/LoraSignalIndicator.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/LoraSignalIndicator.kt @@ -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 ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt b/app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt index c8b493aae..2ddfe8428 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/MessageList.kt @@ -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 + } ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt index aa5006b7b..007978590 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt @@ -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 ) } }