kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat(config): implement excluded modules validation (#1460)
* feat(config): implement excluded modules validation * feat: hide excluded configs from metadata * refactor: save local metadata from WantConfig * refactor: delete metadata from deleted nodes * fix: always request metadata for admin routes * feat: show node firmware when metadata is available * refactor: rename filter function * feat: add `ServiceAction` request metadatapull/1517/head
rodzic
bdefbc3ce2
commit
60e7e18116
|
@ -0,0 +1,564 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 16,
|
||||
"identityHash": "626fc53854f129654c1007b86d9fdda0",
|
||||
"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, PRIMARY KEY(`myNodeNum`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "myNodeNum",
|
||||
"columnName": "myNodeNum",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "model",
|
||||
"columnName": "model",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "firmwareVersion",
|
||||
"columnName": "firmwareVersion",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"myNodeNum"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "shortName",
|
||||
"columnName": "short_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"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, '626fc53854f129654c1007b86d9fdda0')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -24,8 +24,10 @@ import com.geeksville.mesh.database.MeshtasticDatabase
|
|||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.NodeSortOption
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
|
@ -40,6 +42,18 @@ class NodeInfoDaoTest {
|
|||
private lateinit var database: MeshtasticDatabase
|
||||
private lateinit var nodeInfoDao: NodeInfoDao
|
||||
|
||||
private val unknownNode = NodeEntity(
|
||||
num = 7,
|
||||
user = user {
|
||||
id = "!a1b2c3d4"
|
||||
longName = "Meshtastic c3d4"
|
||||
shortName = "c3d4"
|
||||
hwModel = MeshProtos.HardwareModel.UNSET
|
||||
},
|
||||
longName = "Meshtastic c3d4",
|
||||
shortName = null // Dao filter for includeUnknown
|
||||
)
|
||||
|
||||
private val ourNode = NodeEntity(
|
||||
num = 8,
|
||||
user = user {
|
||||
|
@ -79,7 +93,7 @@ class NodeInfoDaoTest {
|
|||
39.952583 to -75.165222, // Philadelphia
|
||||
)
|
||||
|
||||
private val testNodes = listOf(ourNode) + testPositions.mapIndexed { index, pos ->
|
||||
private val testNodes = listOf(ourNode, unknownNode) + testPositions.mapIndexed { index, pos ->
|
||||
NodeEntity(
|
||||
num = 9 + index,
|
||||
user = user {
|
||||
|
@ -89,7 +103,7 @@ class NodeInfoDaoTest {
|
|||
hwModel = MeshProtos.HardwareModel.ANDROID_SIM
|
||||
isLicensed = false
|
||||
},
|
||||
longName = "Kevin Mester$index", shortName = if (index == 2) null else "KM$index",
|
||||
longName = "Kevin Mester$index", shortName = "KM$index",
|
||||
latitude = pos.first, longitude = pos.second,
|
||||
lastHeard = 9 + index,
|
||||
)
|
||||
|
@ -124,18 +138,18 @@ class NodeInfoDaoTest {
|
|||
sort = sort.sqlValue,
|
||||
filter = filter,
|
||||
includeUnknown = includeUnknown,
|
||||
).first().filter { it != ourNode }
|
||||
).map { list -> list.map { it.toModel() } }.first().filter { it.num != ourNode.num }
|
||||
|
||||
@Test // node list size
|
||||
fun testNodeListSize() = runBlocking {
|
||||
val nodes = nodeInfoDao.nodeDBbyNum().first()
|
||||
assertEquals(11, nodes.size)
|
||||
assertEquals(12, nodes.size)
|
||||
}
|
||||
|
||||
@Test // nodeDBbyNum() re-orders our node at the top of the list
|
||||
fun testOurNodeInfoIsFirst() = runBlocking {
|
||||
val nodes = nodeInfoDao.nodeDBbyNum().first()
|
||||
assertEquals(ourNode, nodes.values.first())
|
||||
assertEquals(ourNode.num, nodes.values.first().node.num)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -155,8 +169,9 @@ class NodeInfoDaoTest {
|
|||
@Test
|
||||
fun testSortByDistance() = runBlocking {
|
||||
val nodes = getNodes(sort = NodeSortOption.DISTANCE)
|
||||
fun NodeEntity.toNode() = Node(num = num, user = user, position = position)
|
||||
val sortedNodes = nodes.sortedWith( // nodes with invalid (null) positions at the end
|
||||
compareBy<NodeEntity> { it.validPosition == null }.thenBy { it.distance(ourNode) }
|
||||
compareBy<Node> { it.validPosition == null }.thenBy { it.distance(ourNode.toNode()) }
|
||||
)
|
||||
assertEquals(sortedNodes, nodes)
|
||||
}
|
||||
|
@ -185,7 +200,7 @@ class NodeInfoDaoTest {
|
|||
@Test
|
||||
fun testIncludeUnknownIsTrue() = runBlocking {
|
||||
val nodes = getNodes(includeUnknown = true)
|
||||
val containsUnsetNode = nodes.any { it.shortName == null }
|
||||
val containsUnsetNode = nodes.any { it.isUnknownUser }
|
||||
assertTrue(containsUnsetNode)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,4 +114,19 @@ class Converters : Logging {
|
|||
fun paxCounterToBytes(value: PaxcountProtos.Paxcount): ByteArray? {
|
||||
return value.toByteArray()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToMetadata(bytes: ByteArray): MeshProtos.DeviceMetadata {
|
||||
return try {
|
||||
MeshProtos.DeviceMetadata.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
errormsg("bytesToMetadata TypeConverter error:", ex)
|
||||
MeshProtos.DeviceMetadata.getDefaultInstance()
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun metadataToBytes(value: MeshProtos.DeviceMetadata): ByteArray? {
|
||||
return value.toByteArray()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import com.geeksville.mesh.database.dao.NodeInfoDao
|
|||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import com.geeksville.mesh.database.entity.ContactSettings
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
|
@ -46,6 +47,7 @@ import com.geeksville.mesh.database.entity.ReactionEntity
|
|||
MeshLog::class,
|
||||
QuickChatAction::class,
|
||||
ReactionEntity::class,
|
||||
MetadataEntity::class,
|
||||
],
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 3, to = 4),
|
||||
|
@ -60,8 +62,9 @@ import com.geeksville.mesh.database.entity.ReactionEntity
|
|||
AutoMigration(from = 12, to = 13, spec = AutoMigration12to13::class),
|
||||
AutoMigration(from = 13, to = 14),
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
],
|
||||
version = 15,
|
||||
version = 16,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
|
@ -23,14 +23,19 @@ import com.geeksville.mesh.CoroutineDispatchers
|
|||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.NodeSortOption
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -49,15 +54,20 @@ class NodeRepository @Inject constructor(
|
|||
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, null)
|
||||
|
||||
// our node info
|
||||
private val _ourNodeInfo = MutableStateFlow<NodeEntity?>(null)
|
||||
val ourNodeInfo: StateFlow<NodeEntity?> get() = _ourNodeInfo
|
||||
private val _ourNodeInfo = MutableStateFlow<Node?>(null)
|
||||
val ourNodeInfo: StateFlow<Node?> get() = _ourNodeInfo
|
||||
|
||||
// The unique userId of our node
|
||||
private val _myId = MutableStateFlow<String?>(null)
|
||||
val myId: StateFlow<String?> get() = _myId
|
||||
|
||||
// A map from nodeNum to NodeEntity
|
||||
val nodeDBbyNum: StateFlow<Map<Int, NodeEntity>> = nodeInfoDao.nodeDBbyNum()
|
||||
fun getNodeDBbyNum() = nodeInfoDao.nodeDBbyNum()
|
||||
.map { map -> map.mapValues { (_, it) -> it.toEntity() } }
|
||||
|
||||
// A map from nodeNum to Node
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val nodeDBbyNum: StateFlow<Map<Int, Node>> = nodeInfoDao.nodeDBbyNum()
|
||||
.mapLatest { map -> map.mapValues { (_, it) -> it.toModel() } }
|
||||
.onEach {
|
||||
val ourNodeInfo = it.values.firstOrNull()
|
||||
_ourNodeInfo.value = ourNodeInfo
|
||||
|
@ -67,8 +77,8 @@ class NodeRepository @Inject constructor(
|
|||
.conflate()
|
||||
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
fun getNode(userId: String): NodeEntity = nodeDBbyNum.value.values.find { it.user.id == userId }
|
||||
?: NodeEntity(
|
||||
fun getNode(userId: String): Node = nodeDBbyNum.value.values.find { it.user.id == userId }
|
||||
?: Node(
|
||||
num = DataPacket.idToDefaultNodeNum(userId) ?: 0,
|
||||
user = getUser(userId),
|
||||
)
|
||||
|
@ -84,6 +94,7 @@ class NodeRepository @Inject constructor(
|
|||
.setHwModel(MeshProtos.HardwareModel.UNSET)
|
||||
.build()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun getNodes(
|
||||
sort: NodeSortOption = NodeSortOption.LAST_HEARD,
|
||||
filter: String = "",
|
||||
|
@ -92,7 +103,7 @@ class NodeRepository @Inject constructor(
|
|||
sort = sort.sqlValue,
|
||||
filter = filter,
|
||||
includeUnknown = includeUnknown,
|
||||
).flowOn(dispatchers.io).conflate()
|
||||
).mapLatest { list -> list.map { it.toModel() } }.flowOn(dispatchers.io).conflate()
|
||||
|
||||
suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) {
|
||||
nodeInfoDao.upsert(node)
|
||||
|
@ -107,5 +118,10 @@ class NodeRepository @Inject constructor(
|
|||
|
||||
suspend fun deleteNode(num: Int) = withContext(dispatchers.io) {
|
||||
nodeInfoDao.deleteNode(num)
|
||||
nodeInfoDao.deleteMetadata(num)
|
||||
}
|
||||
|
||||
suspend fun insertMetadata(metadata: MetadataEntity) = withContext(dispatchers.io) {
|
||||
nodeInfoDao.upsert(metadata)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,11 +22,15 @@ import androidx.room.Insert
|
|||
import androidx.room.MapColumn
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeWithRelations
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Dao
|
||||
interface NodeInfoDao {
|
||||
|
||||
|
@ -49,7 +53,8 @@ interface NodeInfoDao {
|
|||
last_heard DESC
|
||||
"""
|
||||
)
|
||||
fun nodeDBbyNum(): Flow<Map<@MapColumn(columnName = "num") Int, NodeEntity>>
|
||||
@Transaction
|
||||
fun nodeDBbyNum(): Flow<Map<@MapColumn(columnName = "num") Int, NodeWithRelations>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
|
@ -92,11 +97,12 @@ interface NodeInfoDao {
|
|||
last_heard DESC
|
||||
"""
|
||||
)
|
||||
@Transaction
|
||||
fun getNodes(
|
||||
sort: String,
|
||||
filter: String,
|
||||
includeUnknown: Boolean,
|
||||
): Flow<List<NodeEntity>>
|
||||
): Flow<List<NodeWithRelations>>
|
||||
|
||||
@Upsert
|
||||
fun upsert(node: NodeEntity)
|
||||
|
@ -109,4 +115,10 @@ interface NodeInfoDao {
|
|||
|
||||
@Query("DELETE FROM nodes WHERE num=:num")
|
||||
fun deleteNode(num: Int)
|
||||
|
||||
@Upsert
|
||||
fun upsert(meta: MetadataEntity)
|
||||
|
||||
@Query("DELETE FROM metadata WHERE num=:num")
|
||||
fun deleteMetadata(num: Int)
|
||||
}
|
||||
|
|
|
@ -17,11 +17,12 @@
|
|||
|
||||
package com.geeksville.mesh.database.entity
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
|
||||
import androidx.room.Relation
|
||||
import com.geeksville.mesh.DeviceMetrics
|
||||
import com.geeksville.mesh.EnvironmentMetrics
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
|
@ -31,16 +32,72 @@ import com.geeksville.mesh.PaxcountProtos
|
|||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.util.bearing
|
||||
import com.geeksville.mesh.util.GPSFormat
|
||||
import com.geeksville.mesh.util.latLongToMeter
|
||||
import com.geeksville.mesh.util.toDistanceString
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
data class NodeWithRelations(
|
||||
@Embedded val node: NodeEntity,
|
||||
@Relation(entity = MetadataEntity::class, parentColumn = "num", entityColumn = "num")
|
||||
val metadata: MetadataEntity? = null,
|
||||
) {
|
||||
fun toModel() = with(node) {
|
||||
Node(
|
||||
num = num,
|
||||
metadata = metadata?.proto,
|
||||
user = user,
|
||||
position = position,
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceMetrics = deviceTelemetry.deviceMetrics,
|
||||
channel = channel,
|
||||
viaMqtt = viaMqtt,
|
||||
hopsAway = hopsAway,
|
||||
isFavorite = isFavorite,
|
||||
isIgnored = isIgnored,
|
||||
environmentMetrics = environmentTelemetry.environmentMetrics,
|
||||
powerMetrics = powerTelemetry.powerMetrics,
|
||||
paxcounter = paxcounter,
|
||||
)
|
||||
}
|
||||
|
||||
fun toEntity() = with(node) {
|
||||
NodeEntity(
|
||||
num = num,
|
||||
user = user,
|
||||
position = position,
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceTelemetry = deviceTelemetry,
|
||||
channel = channel,
|
||||
viaMqtt = viaMqtt,
|
||||
hopsAway = hopsAway,
|
||||
isFavorite = isFavorite,
|
||||
isIgnored = isIgnored,
|
||||
environmentTelemetry = environmentTelemetry,
|
||||
powerTelemetry = powerTelemetry,
|
||||
paxcounter = paxcounter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(
|
||||
tableName = "metadata",
|
||||
indices = [
|
||||
Index(value = ["num"]),
|
||||
],
|
||||
)
|
||||
data class MetadataEntity(
|
||||
@PrimaryKey val num: Int,
|
||||
@ColumnInfo(name = "proto", typeAffinity = ColumnInfo.BLOB)
|
||||
val proto: MeshProtos.DeviceMetadata,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Entity(tableName = "nodes")
|
||||
data class NodeEntity(
|
||||
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val num: Int, // This is immutable, and used as a key
|
||||
|
||||
|
@ -92,32 +149,9 @@ data class NodeEntity(
|
|||
val environmentMetrics: TelemetryProtos.EnvironmentMetrics
|
||||
get() = environmentTelemetry.environmentMetrics
|
||||
|
||||
val hasEnvironmentMetrics: Boolean
|
||||
get() = environmentMetrics != TelemetryProtos.EnvironmentMetrics.getDefaultInstance()
|
||||
|
||||
val powerMetrics: TelemetryProtos.PowerMetrics
|
||||
get() = powerTelemetry.powerMetrics
|
||||
|
||||
val hasPowerMetrics: Boolean
|
||||
get() = powerMetrics != TelemetryProtos.PowerMetrics.getDefaultInstance()
|
||||
|
||||
val colors: Pair<Int, Int>
|
||||
get() { // returns foreground and background @ColorInt for each 'num'
|
||||
val r = (num and 0xFF0000) shr 16
|
||||
val g = (num and 0x00FF00) shr 8
|
||||
val b = num and 0x0000FF
|
||||
val brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255
|
||||
return (if (brightness > 0.5) Color.BLACK else Color.WHITE) to Color.rgb(r, g, b)
|
||||
}
|
||||
|
||||
val isUnknownUser get() = user.hwModel == MeshProtos.HardwareModel.UNSET
|
||||
val hasPKC get() = !user.publicKey.isEmpty
|
||||
val errorByteString: ByteString get() = ByteString.copyFrom(ByteArray(32) { 0 })
|
||||
val mismatchKey get() = user.publicKey == errorByteString
|
||||
|
||||
val batteryLevel get() = deviceMetrics.batteryLevel
|
||||
val voltage get() = deviceMetrics.voltage
|
||||
val batteryStr get() = if (batteryLevel in 1..100) "$batteryLevel%" else ""
|
||||
|
||||
fun setPosition(p: MeshProtos.Position, defaultTime: Int = currentTime()) {
|
||||
position = p.copy { time = if (p.time != 0) p.time else defaultTime }
|
||||
|
@ -125,75 +159,6 @@ data class NodeEntity(
|
|||
longitude = degD(p.longitudeI)
|
||||
}
|
||||
|
||||
private fun hasValidPosition(): Boolean {
|
||||
return latitude != 0.0 && longitude != 0.0 &&
|
||||
(latitude >= -90 && latitude <= 90.0) &&
|
||||
(longitude >= -180 && longitude <= 180)
|
||||
}
|
||||
|
||||
val validPosition: MeshProtos.Position? get() = position.takeIf { hasValidPosition() }
|
||||
|
||||
// @return distance in meters to some other node (or null if unknown)
|
||||
fun distance(o: NodeEntity): Int? = when {
|
||||
validPosition == null || o.validPosition == null -> null
|
||||
else -> latLongToMeter(latitude, longitude, o.latitude, o.longitude).toInt()
|
||||
}
|
||||
|
||||
// @return a nice human readable string for the distance, or null for unknown
|
||||
fun distanceStr(o: NodeEntity, displayUnits: Int = 0): String? = distance(o)?.let { dist ->
|
||||
val system = DisplayConfig.DisplayUnits.forNumber(displayUnits)
|
||||
return if (dist > 0) dist.toDistanceString(system) else null
|
||||
}
|
||||
|
||||
// @return bearing to the other position in degrees
|
||||
fun bearing(o: NodeEntity?): Int? = when {
|
||||
validPosition == null || o?.validPosition == null -> null
|
||||
else -> bearing(latitude, longitude, o.latitude, o.longitude).toInt()
|
||||
}
|
||||
|
||||
fun gpsString(gpsFormat: Int): String = when (gpsFormat) {
|
||||
DisplayConfig.GpsCoordinateFormat.DEC_VALUE -> GPSFormat.toDEC(latitude, longitude)
|
||||
DisplayConfig.GpsCoordinateFormat.DMS_VALUE -> GPSFormat.toDMS(latitude, longitude)
|
||||
DisplayConfig.GpsCoordinateFormat.UTM_VALUE -> GPSFormat.toUTM(latitude, longitude)
|
||||
DisplayConfig.GpsCoordinateFormat.MGRS_VALUE -> GPSFormat.toMGRS(latitude, longitude)
|
||||
else -> GPSFormat.toDEC(latitude, longitude)
|
||||
}
|
||||
|
||||
private fun TelemetryProtos.EnvironmentMetrics.getDisplayString(isFahrenheit: Boolean): String {
|
||||
val temp = if (temperature != 0f) {
|
||||
if (isFahrenheit) {
|
||||
val fahrenheit = temperature * 1.8F + 32
|
||||
"%.1f°F".format(fahrenheit)
|
||||
} else {
|
||||
"%.1f°C".format(temperature)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val humidity = if (relativeHumidity != 0f) "%.0f%%".format(relativeHumidity) else null
|
||||
val voltage = if (this.voltage != 0f) "%.2fV".format(this.voltage) else null
|
||||
val current = if (current != 0f) "%.1fmA".format(current) else null
|
||||
val iaq = if (iaq != 0) "IAQ: $iaq" else null
|
||||
|
||||
return listOfNotNull(
|
||||
temp,
|
||||
humidity,
|
||||
voltage,
|
||||
current,
|
||||
iaq,
|
||||
).joinToString(" ")
|
||||
}
|
||||
|
||||
private fun PaxcountProtos.Paxcount.getDisplayString() =
|
||||
"PAX: ${ble + wifi} (B:$ble/W:$wifi)".takeIf { ble != 0 && wifi != 0 }
|
||||
|
||||
fun getTelemetryString(isFahrenheit: Boolean = false): String {
|
||||
return listOfNotNull(
|
||||
paxcounter.getDisplayString(),
|
||||
environmentMetrics.getDisplayString(isFahrenheit),
|
||||
).joinToString(" ")
|
||||
}
|
||||
|
||||
/**
|
||||
* true if the device was heard from recently
|
||||
*/
|
||||
|
@ -211,48 +176,48 @@ data class NodeEntity(
|
|||
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun NodeEntity.toNodeInfo() = NodeInfo(
|
||||
num = num,
|
||||
user = MeshUser(
|
||||
id = user.id,
|
||||
longName = user.longName,
|
||||
shortName = user.shortName,
|
||||
hwModel = user.hwModel,
|
||||
role = user.roleValue,
|
||||
).takeIf { user.id.isNotEmpty() },
|
||||
position = Position(
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
altitude = position.altitude,
|
||||
time = position.time,
|
||||
satellitesInView = position.satsInView,
|
||||
groundSpeed = position.groundSpeed,
|
||||
groundTrack = position.groundTrack,
|
||||
precisionBits = position.precisionBits,
|
||||
).takeIf { it.isValid() },
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceMetrics = DeviceMetrics(
|
||||
time = deviceTelemetry.time,
|
||||
batteryLevel = deviceMetrics.batteryLevel,
|
||||
voltage = deviceMetrics.voltage,
|
||||
channelUtilization = deviceMetrics.channelUtilization,
|
||||
airUtilTx = deviceMetrics.airUtilTx,
|
||||
uptimeSeconds = deviceMetrics.uptimeSeconds,
|
||||
),
|
||||
channel = channel,
|
||||
environmentMetrics = EnvironmentMetrics(
|
||||
time = environmentTelemetry.time,
|
||||
temperature = environmentMetrics.temperature,
|
||||
relativeHumidity = environmentMetrics.relativeHumidity,
|
||||
barometricPressure = environmentMetrics.barometricPressure,
|
||||
gasResistance = environmentMetrics.gasResistance,
|
||||
voltage = environmentMetrics.voltage,
|
||||
current = environmentMetrics.current,
|
||||
iaq = environmentMetrics.iaq,
|
||||
),
|
||||
hopsAway = hopsAway,
|
||||
)
|
||||
fun toNodeInfo() = NodeInfo(
|
||||
num = num,
|
||||
user = MeshUser(
|
||||
id = user.id,
|
||||
longName = user.longName,
|
||||
shortName = user.shortName,
|
||||
hwModel = user.hwModel,
|
||||
role = user.roleValue,
|
||||
).takeIf { user.id.isNotEmpty() },
|
||||
position = Position(
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
altitude = position.altitude,
|
||||
time = position.time,
|
||||
satellitesInView = position.satsInView,
|
||||
groundSpeed = position.groundSpeed,
|
||||
groundTrack = position.groundTrack,
|
||||
precisionBits = position.precisionBits,
|
||||
).takeIf { it.isValid() },
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceMetrics = DeviceMetrics(
|
||||
time = deviceTelemetry.time,
|
||||
batteryLevel = deviceMetrics.batteryLevel,
|
||||
voltage = deviceMetrics.voltage,
|
||||
channelUtilization = deviceMetrics.channelUtilization,
|
||||
airUtilTx = deviceMetrics.airUtilTx,
|
||||
uptimeSeconds = deviceMetrics.uptimeSeconds,
|
||||
),
|
||||
channel = channel,
|
||||
environmentMetrics = EnvironmentMetrics(
|
||||
time = environmentTelemetry.time,
|
||||
temperature = environmentMetrics.temperature,
|
||||
relativeHumidity = environmentMetrics.relativeHumidity,
|
||||
barometricPressure = environmentMetrics.barometricPressure,
|
||||
gasResistance = environmentMetrics.gasResistance,
|
||||
voltage = environmentMetrics.voltage,
|
||||
current = environmentMetrics.current,
|
||||
iaq = environmentMetrics.iaq,
|
||||
),
|
||||
hopsAway = hopsAway,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import androidx.room.Relation
|
|||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MeshProtos.User
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.util.getShortDateTime
|
||||
|
||||
data class PacketEntity(
|
||||
|
@ -33,7 +34,7 @@ data class PacketEntity(
|
|||
@Relation(entity = ReactionEntity::class, parentColumn = "packet_id", entityColumn = "reply_id")
|
||||
val reactions: List<ReactionEntity> = emptyList(),
|
||||
) {
|
||||
suspend fun toMessage(getNode: suspend (userId: String?) -> NodeEntity) = with(packet) {
|
||||
suspend fun toMessage(getNode: suspend (userId: String?) -> Node) = with(packet) {
|
||||
Message(
|
||||
uuid = uuid,
|
||||
receivedTime = received_time,
|
||||
|
@ -101,7 +102,7 @@ data class ReactionEntity(
|
|||
)
|
||||
|
||||
private suspend fun ReactionEntity.toReaction(
|
||||
getNode: suspend (userId: String?) -> NodeEntity
|
||||
getNode: suspend (userId: String?) -> Node
|
||||
) = Reaction(
|
||||
replyId = replyId,
|
||||
user = getNode(userId).user,
|
||||
|
@ -110,5 +111,5 @@ private suspend fun ReactionEntity.toReaction(
|
|||
)
|
||||
|
||||
private suspend fun List<ReactionEntity>.toReaction(
|
||||
getNode: suspend (userId: String?) -> NodeEntity
|
||||
getNode: suspend (userId: String?) -> Node
|
||||
) = this.map { it.toReaction(getNode) }
|
||||
|
|
|
@ -21,7 +21,6 @@ import androidx.annotation.StringRes
|
|||
import com.geeksville.mesh.MeshProtos.Routing
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.Reaction
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
|
@ -49,7 +48,7 @@ fun getStringResFrom(routingError: Int): Int = when (routingError) {
|
|||
data class Message(
|
||||
val uuid: Long,
|
||||
val receivedTime: Long,
|
||||
val node: NodeEntity,
|
||||
val node: Node,
|
||||
val text: String,
|
||||
val time: String,
|
||||
val read: Boolean,
|
||||
|
|
|
@ -38,7 +38,6 @@ import com.geeksville.mesh.TelemetryProtos.Telemetry
|
|||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.model.map.CustomTileSource
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.ui.Route
|
||||
|
@ -71,7 +70,7 @@ data class MetricsState(
|
|||
val isManaged: Boolean = true,
|
||||
val isFahrenheit: Boolean = false,
|
||||
val displayUnits: DisplayUnits = DisplayUnits.METRIC,
|
||||
val node: NodeEntity? = null,
|
||||
val node: Node? = null,
|
||||
val deviceMetrics: List<Telemetry> = emptyList(),
|
||||
val environmentMetrics: List<Telemetry> = emptyList(),
|
||||
val signalMetrics: List<MeshPacket> = emptyList(),
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.model
|
||||
|
||||
import android.graphics.Color
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.TelemetryProtos.DeviceMetrics
|
||||
import com.geeksville.mesh.TelemetryProtos.EnvironmentMetrics
|
||||
import com.geeksville.mesh.TelemetryProtos.PowerMetrics
|
||||
import com.geeksville.mesh.util.GPSFormat
|
||||
import com.geeksville.mesh.util.latLongToMeter
|
||||
import com.geeksville.mesh.util.toDistanceString
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
data class Node(
|
||||
val num: Int,
|
||||
val metadata: MeshProtos.DeviceMetadata? = null,
|
||||
val user: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
|
||||
val position: MeshProtos.Position = MeshProtos.Position.getDefaultInstance(),
|
||||
val snr: Float = Float.MAX_VALUE,
|
||||
val rssi: Int = Int.MAX_VALUE,
|
||||
val lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
|
||||
val deviceMetrics: DeviceMetrics = DeviceMetrics.getDefaultInstance(),
|
||||
val channel: Int = 0,
|
||||
val viaMqtt: Boolean = false,
|
||||
val hopsAway: Int = -1,
|
||||
val isFavorite: Boolean = false,
|
||||
val isIgnored: Boolean = false,
|
||||
val environmentMetrics: EnvironmentMetrics = EnvironmentMetrics.getDefaultInstance(),
|
||||
val powerMetrics: PowerMetrics = PowerMetrics.getDefaultInstance(),
|
||||
val paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(),
|
||||
) {
|
||||
val colors: Pair<Int, Int>
|
||||
get() { // returns foreground and background @ColorInt for each 'num'
|
||||
val r = (num and 0xFF0000) shr 16
|
||||
val g = (num and 0x00FF00) shr 8
|
||||
val b = num and 0x0000FF
|
||||
val brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255
|
||||
return (if (brightness > 0.5) Color.BLACK else Color.WHITE) to Color.rgb(r, g, b)
|
||||
}
|
||||
|
||||
val isUnknownUser get() = user.hwModel == MeshProtos.HardwareModel.UNSET
|
||||
val hasPKC get() = !user.publicKey.isEmpty
|
||||
val errorByteString: ByteString get() = ByteString.copyFrom(ByteArray(32) { 0 })
|
||||
val mismatchKey get() = user.publicKey == errorByteString
|
||||
|
||||
val hasEnvironmentMetrics: Boolean
|
||||
get() = environmentMetrics != EnvironmentMetrics.getDefaultInstance()
|
||||
|
||||
val hasPowerMetrics: Boolean
|
||||
get() = powerMetrics != PowerMetrics.getDefaultInstance()
|
||||
|
||||
val batteryLevel get() = deviceMetrics.batteryLevel
|
||||
val voltage get() = deviceMetrics.voltage
|
||||
val batteryStr get() = if (batteryLevel in 1..100) "$batteryLevel%" else ""
|
||||
|
||||
val latitude get() = position.latitudeI * 1e-7
|
||||
val longitude get() = position.longitudeI * 1e-7
|
||||
|
||||
private fun hasValidPosition(): Boolean {
|
||||
return latitude != 0.0 && longitude != 0.0 &&
|
||||
(latitude >= -90 && latitude <= 90.0) &&
|
||||
(longitude >= -180 && longitude <= 180)
|
||||
}
|
||||
|
||||
val validPosition: MeshProtos.Position? get() = position.takeIf { hasValidPosition() }
|
||||
|
||||
// @return distance in meters to some other node (or null if unknown)
|
||||
fun distance(o: Node): Int? = when {
|
||||
validPosition == null || o.validPosition == null -> null
|
||||
else -> latLongToMeter(latitude, longitude, o.latitude, o.longitude).toInt()
|
||||
}
|
||||
|
||||
// @return a nice human readable string for the distance, or null for unknown
|
||||
fun distanceStr(o: Node, displayUnits: Int = 0): String? = distance(o)?.let { dist ->
|
||||
val system = DisplayConfig.DisplayUnits.forNumber(displayUnits)
|
||||
return if (dist > 0) dist.toDistanceString(system) else null
|
||||
}
|
||||
|
||||
// @return bearing to the other position in degrees
|
||||
fun bearing(o: Node?): Int? = when {
|
||||
validPosition == null || o?.validPosition == null -> null
|
||||
else -> com.geeksville.mesh.util.bearing(latitude, longitude, o.latitude, o.longitude).toInt()
|
||||
}
|
||||
|
||||
fun gpsString(gpsFormat: Int): String = when (gpsFormat) {
|
||||
DisplayConfig.GpsCoordinateFormat.DEC_VALUE -> GPSFormat.toDEC(latitude, longitude)
|
||||
DisplayConfig.GpsCoordinateFormat.DMS_VALUE -> GPSFormat.toDMS(latitude, longitude)
|
||||
DisplayConfig.GpsCoordinateFormat.UTM_VALUE -> GPSFormat.toUTM(latitude, longitude)
|
||||
DisplayConfig.GpsCoordinateFormat.MGRS_VALUE -> GPSFormat.toMGRS(latitude, longitude)
|
||||
else -> GPSFormat.toDEC(latitude, longitude)
|
||||
}
|
||||
|
||||
private fun EnvironmentMetrics.getDisplayString(isFahrenheit: Boolean): String {
|
||||
val temp = if (temperature != 0f) {
|
||||
if (isFahrenheit) {
|
||||
val fahrenheit = temperature * 1.8F + 32
|
||||
"%.1f°F".format(fahrenheit)
|
||||
} else {
|
||||
"%.1f°C".format(temperature)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val humidity = if (relativeHumidity != 0f) "%.0f%%".format(relativeHumidity) else null
|
||||
val voltage = if (this.voltage != 0f) "%.2fV".format(this.voltage) else null
|
||||
val current = if (current != 0f) "%.1fmA".format(current) else null
|
||||
val iaq = if (iaq != 0) "IAQ: $iaq" else null
|
||||
|
||||
return listOfNotNull(
|
||||
temp,
|
||||
humidity,
|
||||
voltage,
|
||||
current,
|
||||
iaq,
|
||||
).joinToString(" ")
|
||||
}
|
||||
|
||||
private fun PaxcountProtos.Paxcount.getDisplayString() =
|
||||
"PAX: ${ble + wifi} (B:$ble/W:$wifi)".takeIf { ble != 0 && wifi != 0 }
|
||||
|
||||
fun getTelemetryString(isFahrenheit: Boolean = false): String {
|
||||
return listOfNotNull(
|
||||
paxcounter.getDisplayString(),
|
||||
environmentMetrics.getDisplayString(isFahrenheit),
|
||||
).joinToString(" ")
|
||||
}
|
||||
}
|
|
@ -38,7 +38,6 @@ import com.geeksville.mesh.R
|
|||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.deviceProfile
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
|
@ -56,6 +55,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
|
@ -73,7 +73,7 @@ data class RadioConfigState(
|
|||
val isLocal: Boolean = false,
|
||||
val connected: Boolean = false,
|
||||
val route: String = "",
|
||||
val metadata: MeshProtos.DeviceMetadata = MeshProtos.DeviceMetadata.getDefaultInstance(),
|
||||
val metadata: MeshProtos.DeviceMetadata? = null,
|
||||
val userConfig: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
|
||||
val channelList: List<ChannelProtos.ChannelSettings> = emptyList(),
|
||||
val radioConfig: ConfigProtos.Config = config {},
|
||||
|
@ -81,9 +81,7 @@ data class RadioConfigState(
|
|||
val ringtone: String = "",
|
||||
val cannedMessageMessages: String = "",
|
||||
val responseState: ResponseState<Boolean> = ResponseState.Empty,
|
||||
) {
|
||||
fun hasMetadata() = metadata != MeshProtos.DeviceMetadata.getDefaultInstance()
|
||||
}
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class RadioConfigViewModel @Inject constructor(
|
||||
|
@ -94,8 +92,8 @@ class RadioConfigViewModel @Inject constructor(
|
|||
private val meshService: IMeshService? get() = radioConfigRepository.meshService
|
||||
|
||||
private val destNum = savedStateHandle.toRoute<Route.RadioConfig>().destNum
|
||||
private val _destNode = MutableStateFlow<NodeEntity?>(null)
|
||||
val destNode: StateFlow<NodeEntity?> get() = _destNode
|
||||
private val _destNode = MutableStateFlow<Node?>(null)
|
||||
val destNode: StateFlow<Node?> get() = _destNode
|
||||
|
||||
private val requestIds = MutableStateFlow(hashSetOf<Int>())
|
||||
private val _radioConfigState = MutableStateFlow(RadioConfigState())
|
||||
|
@ -106,9 +104,14 @@ class RadioConfigViewModel @Inject constructor(
|
|||
|
||||
init {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
radioConfigRepository.nodeDBbyNum.mapLatest { nodes ->
|
||||
nodes[destNum] ?: nodes.values.firstOrNull()
|
||||
}.onEach { _destNode.value = it }.launchIn(viewModelScope)
|
||||
radioConfigRepository.nodeDBbyNum
|
||||
.mapLatest { nodes -> nodes[destNum] ?: nodes.values.firstOrNull() }
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
_destNode.value = it
|
||||
_radioConfigState.update { state -> state.copy(metadata = it?.metadata) }
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
radioConfigRepository.deviceProfileFlow.onEach {
|
||||
_currentDeviceProfile.value = it
|
||||
|
@ -322,7 +325,7 @@ class RadioConfigViewModel @Inject constructor(
|
|||
when (route) {
|
||||
AdminRoute.REBOOT.name -> requestReboot(destNum)
|
||||
AdminRoute.SHUTDOWN.name -> with(radioConfigState.value) {
|
||||
if (hasMetadata() && !metadata.canShutdown) {
|
||||
if (metadata != null && !metadata.canShutdown) {
|
||||
sendError(R.string.cant_shutdown)
|
||||
} else {
|
||||
requestShutdown(destNum)
|
||||
|
@ -334,15 +337,6 @@ class RadioConfigViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getSessionPasskey(destNum: Int) {
|
||||
if (radioConfigState.value.hasMetadata()) {
|
||||
sendAdminRequest(destNum)
|
||||
} else {
|
||||
getConfig(destNum, AdminProtos.AdminMessage.ConfigType.SESSIONKEY_CONFIG_VALUE)
|
||||
setResponseStateTotal(2)
|
||||
}
|
||||
}
|
||||
|
||||
fun setFixedPosition(position: Position) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
try {
|
||||
|
@ -441,10 +435,15 @@ class RadioConfigViewModel @Inject constructor(
|
|||
fun setResponseStateLoading(route: Enum<*>) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
|
||||
_radioConfigState.value = RadioConfigState(
|
||||
route = route.name,
|
||||
responseState = ResponseState.Loading(),
|
||||
)
|
||||
_radioConfigState.update {
|
||||
RadioConfigState(
|
||||
isLocal = it.isLocal,
|
||||
connected = it.connected,
|
||||
route = route.name,
|
||||
metadata = it.metadata,
|
||||
responseState = ResponseState.Loading(),
|
||||
)
|
||||
}
|
||||
|
||||
when (route) {
|
||||
ConfigRoute.USER -> getOwner(destNum)
|
||||
|
@ -456,7 +455,10 @@ class RadioConfigViewModel @Inject constructor(
|
|||
setResponseStateTotal(maxChannels + 1)
|
||||
}
|
||||
|
||||
is AdminRoute -> getSessionPasskey(destNum)
|
||||
is AdminRoute -> {
|
||||
getConfig(destNum, AdminProtos.AdminMessage.ConfigType.SESSIONKEY_CONFIG_VALUE)
|
||||
setResponseStateTotal(2)
|
||||
}
|
||||
|
||||
is ConfigRoute -> {
|
||||
if (route == ConfigRoute.LORA) {
|
||||
|
|
|
@ -52,7 +52,6 @@ import com.geeksville.mesh.database.NodeRepository
|
|||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.database.QuickChatActionRepository
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
|
@ -235,7 +234,7 @@ class UIViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val nodeList: StateFlow<List<NodeEntity>> = nodesUiState.flatMapLatest { state ->
|
||||
val nodeList: StateFlow<List<Node>> = nodesUiState.flatMapLatest { state ->
|
||||
nodeDB.getNodes(state.sort, state.filter, state.includeUnknown)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
|
@ -245,7 +244,7 @@ class UIViewModel @Inject constructor(
|
|||
|
||||
// hardware info about our local device (can be null)
|
||||
val myNodeInfo: StateFlow<MyNodeEntity?> get() = nodeDB.myNodeInfo
|
||||
val ourNodeInfo: StateFlow<NodeEntity?> get() = nodeDB.ourNodeInfo
|
||||
val ourNodeInfo: StateFlow<Node?> get() = nodeDB.ourNodeInfo
|
||||
|
||||
val nodesWithPosition get() = nodeDB.nodeDBbyNum.value.values.filter { it.validPosition != null }
|
||||
|
||||
|
@ -484,7 +483,7 @@ class UIViewModel @Inject constructor(
|
|||
updateLoraConfig { it.copy { region = value } }
|
||||
}
|
||||
|
||||
fun ignoreNode(node: NodeEntity) = viewModelScope.launch {
|
||||
fun ignoreNode(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
radioConfigRepository.onServiceAction(ServiceAction.Ignore(node))
|
||||
} catch (ex: RemoteException) {
|
||||
|
|
|
@ -25,12 +25,15 @@ import com.geeksville.mesh.ConfigProtos.Config
|
|||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import com.geeksville.mesh.MeshProtos.DeviceMetadata
|
||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
|
||||
import com.geeksville.mesh.database.NodeRepository
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.deviceProfile
|
||||
import com.geeksville.mesh.database.NodeRepository
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.getChannelUrl
|
||||
import com.geeksville.mesh.service.MeshService.ConnectionState
|
||||
import com.geeksville.mesh.service.ServiceAction
|
||||
|
@ -40,6 +43,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -70,16 +74,20 @@ class RadioConfigRepository @Inject constructor(
|
|||
val myNodeInfo: StateFlow<MyNodeEntity?> get() = nodeDB.myNodeInfo
|
||||
|
||||
/**
|
||||
* Flow representing the [NodeEntity] database.
|
||||
* Flow representing the [Node] database.
|
||||
*/
|
||||
val nodeDBbyNum: StateFlow<Map<Int, NodeEntity>> get() = nodeDB.nodeDBbyNum
|
||||
val nodeDBbyNum: StateFlow<Map<Int, Node>> get() = nodeDB.nodeDBbyNum
|
||||
|
||||
fun getUser(nodeNum: Int) = nodeDB.getUser(nodeNum)
|
||||
|
||||
suspend fun getNodeDBbyNum() = nodeDB.getNodeDBbyNum().first()
|
||||
suspend fun upsert(node: NodeEntity) = nodeDB.upsert(node)
|
||||
suspend fun installNodeDB(mi: MyNodeEntity, nodes: List<NodeEntity>) {
|
||||
nodeDB.installNodeDB(mi, nodes)
|
||||
}
|
||||
suspend fun insertMetadata(fromNum: Int, metadata: DeviceMetadata) {
|
||||
nodeDB.insertMetadata(MetadataEntity(fromNum, metadata))
|
||||
}
|
||||
|
||||
/**
|
||||
* Flow representing the [ChannelSet] data store.
|
||||
|
@ -195,7 +203,7 @@ class RadioConfigRepository @Inject constructor(
|
|||
serviceRepository.emitMeshPacket(packet)
|
||||
}
|
||||
|
||||
val serviceAction: SharedFlow<ServiceAction> get() = serviceRepository.serviceAction
|
||||
val serviceAction: Flow<ServiceAction> get() = serviceRepository.serviceAction
|
||||
|
||||
suspend fun onServiceAction(action: ServiceAction) = coroutineScope {
|
||||
serviceRepository.onServiceAction(action)
|
||||
|
|
|
@ -26,17 +26,38 @@ import android.os.IBinder
|
|||
import android.os.RemoteException
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.location.LocationCompat
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.AdminProtos
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.MeshProtos.ToRadio
|
||||
import com.geeksville.mesh.MeshUser
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.ModuleConfigProtos
|
||||
import com.geeksville.mesh.MyNodeInfo
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.StoreAndForwardProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.TelemetryProtos.LocalStats
|
||||
import com.geeksville.mesh.analytics.DataPair
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.android.hasLocationPermission
|
||||
import com.geeksville.mesh.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
|
@ -44,15 +65,22 @@ import com.geeksville.mesh.database.entity.MyNodeEntity
|
|||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.ReactionEntity
|
||||
import com.geeksville.mesh.database.entity.toNodeInfo
|
||||
import com.geeksville.mesh.fromRadio
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.getTracerouteResponse
|
||||
import com.geeksville.mesh.position
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.repository.location.LocationRepository
|
||||
import com.geeksville.mesh.repository.network.MQTTRepository
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import com.geeksville.mesh.repository.radio.RadioServiceConnectionState
|
||||
import com.geeksville.mesh.util.*
|
||||
import com.geeksville.mesh.telemetry
|
||||
import com.geeksville.mesh.user
|
||||
import com.geeksville.mesh.util.anonymize
|
||||
import com.geeksville.mesh.util.toOneLineString
|
||||
import com.geeksville.mesh.util.toPIIString
|
||||
import com.geeksville.mesh.util.toRemoteExceptions
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import dagger.Lazy
|
||||
|
@ -77,7 +105,8 @@ import javax.inject.Inject
|
|||
import kotlin.math.absoluteValue
|
||||
|
||||
sealed class ServiceAction {
|
||||
data class Ignore(val node: NodeEntity) : ServiceAction()
|
||||
data class GetDeviceMetadata(val destNum: Int) : ServiceAction()
|
||||
data class Ignore(val node: Node) : ServiceAction()
|
||||
data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction()
|
||||
}
|
||||
|
||||
|
@ -302,12 +331,8 @@ class MeshService : Service(), Logging {
|
|||
.launchIn(serviceScope)
|
||||
radioConfigRepository.channelSetFlow.onEach { channelSet = it }
|
||||
.launchIn(serviceScope)
|
||||
radioConfigRepository.serviceAction.onEach { action ->
|
||||
when (action) {
|
||||
is ServiceAction.Ignore -> ignoreNode(action.node)
|
||||
is ServiceAction.Reaction -> sendReaction(action)
|
||||
}
|
||||
}.launchIn(serviceScope)
|
||||
radioConfigRepository.serviceAction.onEach(::onServiceAction)
|
||||
.launchIn(serviceScope)
|
||||
|
||||
loadSettings() // Load our last known node DB
|
||||
|
||||
|
@ -375,10 +400,10 @@ class MeshService : Service(), Logging {
|
|||
// BEGINNING OF MODEL - FIXME, move elsewhere
|
||||
//
|
||||
|
||||
private fun loadSettings() {
|
||||
private fun loadSettings() = serviceScope.handledLaunch {
|
||||
discardNodeDB() // Get rid of any old state
|
||||
myNodeInfo = radioConfigRepository.myNodeInfo.value
|
||||
nodeDBbyNodeNum.putAll(radioConfigRepository.nodeDBbyNum.value)
|
||||
nodeDBbyNodeNum.putAll(radioConfigRepository.getNodeDBbyNum())
|
||||
// Note: we do not haveNodeDB = true because that means we've got a valid db from a real device (rather than this possibly stale hint)
|
||||
}
|
||||
|
||||
|
@ -808,15 +833,17 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage) {
|
||||
if (fromNodeNum == myNodeNum) {
|
||||
when (a.payloadVariantCase) {
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
|
||||
when (a.payloadVariantCase) {
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
|
||||
if (fromNodeNum == myNodeNum) {
|
||||
val response = a.getConfigResponse
|
||||
debug("Admin: received config ${response.payloadVariantCase}")
|
||||
setLocalConfig(response)
|
||||
}
|
||||
}
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
|
||||
if (fromNodeNum == myNodeNum) {
|
||||
val mi = myNodeInfo
|
||||
if (mi != null) {
|
||||
val ch = a.getChannelResponse
|
||||
|
@ -827,12 +854,19 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
}
|
||||
}
|
||||
else -> warn("No special processing needed for ${a.payloadVariantCase}")
|
||||
}
|
||||
} else {
|
||||
debug("Admin: Received session_passkey from $fromNodeNum")
|
||||
sessionPasskey = a.sessionPasskey
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_DEVICE_METADATA_RESPONSE -> {
|
||||
debug("Admin: received DeviceMetadata from $fromNodeNum")
|
||||
serviceScope.handledLaunch {
|
||||
radioConfigRepository.insertMetadata(fromNodeNum, a.getDeviceMetadataResponse)
|
||||
}
|
||||
}
|
||||
|
||||
else -> warn("No special processing needed for ${a.payloadVariantCase}")
|
||||
}
|
||||
debug("Admin: Received session_passkey from $fromNodeNum")
|
||||
sessionPasskey = a.sessionPasskey
|
||||
}
|
||||
|
||||
// Update our DB of users based on someone sending out a User subpacket
|
||||
|
@ -1144,13 +1178,6 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
private fun clearLocalConfig() {
|
||||
serviceScope.handledLaunch {
|
||||
radioConfigRepository.clearLocalConfig()
|
||||
radioConfigRepository.clearLocalModuleConfig()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChannelSettings(ch: ChannelProtos.Channel) = serviceScope.handledLaunch {
|
||||
radioConfigRepository.updateChannelSettings(ch)
|
||||
}
|
||||
|
@ -1476,31 +1503,33 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
private var rawMyNodeInfo: MeshProtos.MyNodeInfo? = null
|
||||
private var rawDeviceMetadata: MeshProtos.DeviceMetadata? = null
|
||||
|
||||
/** Regenerate the myNodeInfo model. We call this twice. Once after we receive myNodeInfo from the device
|
||||
* and again after we have the node DB (which might allow us a better notion of our HwModel.
|
||||
*/
|
||||
private fun regenMyNodeInfo() {
|
||||
private fun regenMyNodeInfo(metadata: MeshProtos.DeviceMetadata) {
|
||||
val myInfo = rawMyNodeInfo
|
||||
if (myInfo != null) {
|
||||
val mi = with(myInfo) {
|
||||
MyNodeEntity(
|
||||
myNodeNum = myNodeNum,
|
||||
model = when (val hwModel = rawDeviceMetadata?.hwModel) {
|
||||
model = when (val hwModel = metadata.hwModel) {
|
||||
null, MeshProtos.HardwareModel.UNSET -> null
|
||||
else -> hwModel.name.replace('_', '-').replace('p', '.').lowercase()
|
||||
},
|
||||
firmwareVersion = rawDeviceMetadata?.firmwareVersion,
|
||||
firmwareVersion = metadata.firmwareVersion,
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false, // TODO add check after re-implementing firmware updates
|
||||
currentPacketId = currentPacketId and 0xffffffffL,
|
||||
messageTimeoutMsec = 5 * 60 * 1000, // constants from current firmware code
|
||||
minAppVersion = minAppVersion,
|
||||
maxChannels = 8,
|
||||
hasWifi = rawDeviceMetadata?.hasWifi ?: false,
|
||||
hasWifi = metadata.hasWifi,
|
||||
)
|
||||
}
|
||||
serviceScope.handledLaunch {
|
||||
radioConfigRepository.insertMetadata(mi.myNodeNum, metadata)
|
||||
}
|
||||
newMyNodeInfo = mi
|
||||
}
|
||||
}
|
||||
|
@ -1554,8 +1583,7 @@ class MeshService : Service(), Logging {
|
|||
)
|
||||
insertMeshLog(packetToSave)
|
||||
|
||||
rawDeviceMetadata = metadata
|
||||
regenMyNodeInfo()
|
||||
regenMyNodeInfo(metadata)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1760,7 +1788,21 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
private fun ignoreNode(node: NodeEntity) = toRemoteExceptions {
|
||||
private fun onServiceAction(action: ServiceAction) {
|
||||
when (action) {
|
||||
is ServiceAction.GetDeviceMetadata -> getDeviceMetadata(action.destNum)
|
||||
is ServiceAction.Ignore -> ignoreNode(action.node)
|
||||
is ServiceAction.Reaction -> sendReaction(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeviceMetadata(destNum: Int) = toRemoteExceptions {
|
||||
sendToRadio(newMeshPacketTo(destNum).buildAdminPacket(wantResponse = true) {
|
||||
getDeviceMetadataRequest = true
|
||||
})
|
||||
}
|
||||
|
||||
private fun ignoreNode(node: Node) = toRemoteExceptions {
|
||||
sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
|
||||
if (node.isIgnored) {
|
||||
debug("removing node ${node.num} from ignore list")
|
||||
|
|
|
@ -20,10 +20,12 @@ package com.geeksville.mesh.service
|
|||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -86,10 +88,10 @@ class ServiceRepository @Inject constructor() : Logging {
|
|||
setTracerouteResponse(null)
|
||||
}
|
||||
|
||||
private val _serviceAction = MutableSharedFlow<ServiceAction>()
|
||||
val serviceAction: SharedFlow<ServiceAction> get() = _serviceAction
|
||||
private val _serviceAction = Channel<ServiceAction>()
|
||||
val serviceAction = _serviceAction.receiveAsFlow()
|
||||
|
||||
suspend fun onServiceAction(action: ServiceAction) {
|
||||
_serviceAction.emit(action)
|
||||
_serviceAction.send(action)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,6 +71,7 @@ import androidx.navigation.NavHostController
|
|||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.geeksville.mesh.MeshProtos.DeviceMetadata
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
|
@ -235,6 +236,18 @@ enum class ConfigRoute(val title: String, val route: Route, val icon: ImageVecto
|
|||
LORA("LoRa", Route.LoRa, Icons.Default.CellTower, 5),
|
||||
BLUETOOTH("Bluetooth", Route.Bluetooth, Icons.Default.Bluetooth, 6),
|
||||
SECURITY("Security", Route.Security, Icons.Default.Security, type = 7),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ConfigRoute> = entries.filter {
|
||||
when {
|
||||
metadata == null -> true
|
||||
it == BLUETOOTH -> metadata.hasBluetooth
|
||||
it == NETWORK -> metadata.hasWifi || metadata.hasEthernet
|
||||
else -> true // Include all other routes by default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ModuleConfig (type = AdminProtos.AdminMessage.ModuleConfigType)
|
||||
|
@ -252,6 +265,18 @@ enum class ModuleRoute(val title: String, val route: Route, val icon: ImageVecto
|
|||
AMBIENT_LIGHTING("Ambient Lighting", Route.AmbientLighting, Icons.Default.LightMode, 10),
|
||||
DETECTION_SENSOR("Detection Sensor", Route.DetectionSensor, Icons.Default.Sensors, 11),
|
||||
PAXCOUNTER("Paxcounter", Route.Paxcounter, Icons.Default.PermScanWifi, 12),
|
||||
;
|
||||
|
||||
val bitfield: Int get() = 1 shl ordinal
|
||||
|
||||
companion object {
|
||||
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ModuleRoute> = entries.filter {
|
||||
when (metadata) {
|
||||
null -> true
|
||||
else -> metadata.excludedModules and it.bitfield == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -58,6 +58,7 @@ import androidx.compose.material.icons.filled.KeyOff
|
|||
import androidx.compose.material.icons.filled.LightMode
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Map
|
||||
import androidx.compose.material.icons.filled.Memory
|
||||
import androidx.compose.material.icons.filled.Numbers
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Power
|
||||
|
@ -91,11 +92,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.model.MetricsState
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.preview.NodePreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.theme.AppTheme
|
||||
import com.geeksville.mesh.util.DistanceUnit
|
||||
import com.geeksville.mesh.util.formatAgo
|
||||
|
@ -132,7 +133,7 @@ fun NodeDetailScreen(
|
|||
@Composable
|
||||
private fun NodeDetailList(
|
||||
modifier: Modifier = Modifier,
|
||||
node: NodeEntity,
|
||||
node: Node,
|
||||
metricsState: MetricsState,
|
||||
onNavigate: (Any) -> Unit = {},
|
||||
) {
|
||||
|
@ -257,7 +258,7 @@ private fun DeviceDetailsContent(
|
|||
|
||||
@Composable
|
||||
private fun NodeDetailsContent(
|
||||
node: NodeEntity,
|
||||
node: Node,
|
||||
) {
|
||||
if (node.mismatchKey) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -304,6 +305,13 @@ private fun NodeDetailsContent(
|
|||
value = formatUptime(node.deviceMetrics.uptimeSeconds)
|
||||
)
|
||||
}
|
||||
if (node.metadata != null) {
|
||||
NodeDetailRow(
|
||||
label = "Firmware version",
|
||||
icon = Icons.Default.Memory,
|
||||
value = node.metadata.firmwareVersion.substringBeforeLast(".")
|
||||
)
|
||||
}
|
||||
NodeDetailRow(
|
||||
label = "Last heard",
|
||||
icon = Icons.Default.History,
|
||||
|
@ -413,7 +421,7 @@ private fun InfoCard(
|
|||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
private fun EnvironmentMetrics(
|
||||
node: NodeEntity,
|
||||
node: Node,
|
||||
isFahrenheit: Boolean = false,
|
||||
) = with(node.environmentMetrics) {
|
||||
FlowRow(
|
||||
|
@ -543,7 +551,7 @@ private fun calculateDewPoint(tempCelsius: Float, humidity: Float): Float {
|
|||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun PowerMetrics(node: NodeEntity) = with(node.powerMetrics) {
|
||||
private fun PowerMetrics(node: Node) = with(node.powerMetrics) {
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
|
@ -597,8 +605,8 @@ private fun PowerMetrics(node: NodeEntity) = with(node.powerMetrics) {
|
|||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun NodeDetailsPreview(
|
||||
@PreviewParameter(NodeEntityPreviewParameterProvider::class)
|
||||
node: NodeEntity
|
||||
@PreviewParameter(NodePreviewParameterProvider::class)
|
||||
node: Node
|
||||
) {
|
||||
AppTheme {
|
||||
NodeDetailList(
|
||||
|
|
|
@ -59,14 +59,14 @@ import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
|
|||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.ui.components.NodeMenuAction
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
|
||||
import com.geeksville.mesh.ui.components.NodeMenu
|
||||
import com.geeksville.mesh.ui.components.NodeMenuAction
|
||||
import com.geeksville.mesh.ui.components.SignalInfo
|
||||
import com.geeksville.mesh.ui.compose.ElevationInfo
|
||||
import com.geeksville.mesh.ui.compose.SatelliteCountInfo
|
||||
import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.preview.NodePreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.theme.AppTheme
|
||||
import com.geeksville.mesh.util.toDistanceString
|
||||
|
||||
|
@ -74,8 +74,8 @@ import com.geeksville.mesh.util.toDistanceString
|
|||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun NodeItem(
|
||||
thisNode: NodeEntity?,
|
||||
thatNode: NodeEntity,
|
||||
thisNode: Node?,
|
||||
thatNode: Node,
|
||||
gpsFormat: Int,
|
||||
distanceUnits: Int,
|
||||
tempInFahrenheit: Boolean,
|
||||
|
@ -293,8 +293,8 @@ fun NodeItem(
|
|||
@Preview(showBackground = false)
|
||||
fun NodeInfoSimplePreview() {
|
||||
AppTheme {
|
||||
val thisNode = NodeEntityPreviewParameterProvider().values.first()
|
||||
val thatNode = NodeEntityPreviewParameterProvider().values.last()
|
||||
val thisNode = NodePreviewParameterProvider().values.first()
|
||||
val thatNode = NodePreviewParameterProvider().values.last()
|
||||
NodeItem(
|
||||
thisNode = thisNode,
|
||||
thatNode = thatNode,
|
||||
|
@ -312,11 +312,11 @@ fun NodeInfoSimplePreview() {
|
|||
uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES,
|
||||
)
|
||||
fun NodeInfoPreview(
|
||||
@PreviewParameter(NodeEntityPreviewParameterProvider::class)
|
||||
thatNode: NodeEntity
|
||||
@PreviewParameter(NodePreviewParameterProvider::class)
|
||||
thatNode: Node
|
||||
) {
|
||||
AppTheme {
|
||||
val thisNode = NodeEntityPreviewParameterProvider().values.first()
|
||||
val thisNode = NodePreviewParameterProvider().values.first()
|
||||
Column {
|
||||
Text(
|
||||
text = "Details Collapsed",
|
||||
|
|
|
@ -65,6 +65,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.model.RadioConfigState
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog
|
||||
|
@ -150,8 +151,7 @@ fun RadioConfigScreen(
|
|||
}
|
||||
|
||||
RadioConfigItemList(
|
||||
enabled = state.connected && !isWaiting,
|
||||
isLocal = state.isLocal,
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onRouteClick = { route ->
|
||||
isWaiting = true
|
||||
|
@ -285,28 +285,28 @@ private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Un
|
|||
|
||||
@Composable
|
||||
private fun RadioConfigItemList(
|
||||
enabled: Boolean = true,
|
||||
isLocal: Boolean = true,
|
||||
state: RadioConfigState,
|
||||
modifier: Modifier = Modifier,
|
||||
onRouteClick: (Enum<*>) -> Unit = {},
|
||||
onImport: () -> Unit = {},
|
||||
onExport: () -> Unit = {},
|
||||
) {
|
||||
val enabled = state.connected && !state.responseState.isWaiting()
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
item { PreferenceCategory(stringResource(R.string.device_settings)) }
|
||||
items(ConfigRoute.entries) {
|
||||
items(ConfigRoute.filterExcludedFrom(state.metadata)) {
|
||||
NavCard(title = it.title, icon = it.icon, enabled = enabled) { onRouteClick(it) }
|
||||
}
|
||||
|
||||
item { PreferenceCategory(stringResource(R.string.module_settings)) }
|
||||
items(ModuleRoute.entries) {
|
||||
items(ModuleRoute.filterExcludedFrom(state.metadata)) {
|
||||
NavCard(title = it.title, icon = it.icon, enabled = enabled) { onRouteClick(it) }
|
||||
}
|
||||
|
||||
if (isLocal) {
|
||||
if (state.isLocal) {
|
||||
item {
|
||||
PreferenceCategory("Backup & Restore")
|
||||
NavCard(
|
||||
|
@ -331,5 +331,7 @@ private fun RadioConfigItemList(
|
|||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun RadioSettingsScreenPreview() {
|
||||
RadioConfigItemList()
|
||||
RadioConfigItemList(
|
||||
RadioConfigState(isLocal = true, connected = true)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.components.NodeMenuAction
|
||||
import com.geeksville.mesh.ui.components.NodeFilterTextField
|
||||
|
@ -53,7 +53,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
|||
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
private fun navigateToMessages(node: NodeEntity) = node.user.let { user ->
|
||||
private fun navigateToMessages(node: Node) = node.user.let { user ->
|
||||
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC // TODO use meta.hasPKC
|
||||
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
|
||||
val contactKey = "$channel${user.id}"
|
||||
|
@ -91,7 +91,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
|||
@Suppress("LongMethod")
|
||||
fun NodesScreen(
|
||||
model: UIViewModel = hiltViewModel(),
|
||||
navigateToMessages: (NodeEntity) -> Unit,
|
||||
navigateToMessages: (Node) -> Unit,
|
||||
navigateToNodeDetails: (Int) -> Unit,
|
||||
) {
|
||||
val state by model.nodesUiState.collectAsStateWithLifecycle()
|
||||
|
|
|
@ -36,12 +36,12 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun NodeMenu(
|
||||
node: NodeEntity,
|
||||
node: Node,
|
||||
showFullMenu: Boolean = false,
|
||||
onDismissRequest: () -> Unit,
|
||||
expanded: Boolean = false,
|
||||
|
@ -150,11 +150,11 @@ fun NodeMenu(
|
|||
}
|
||||
|
||||
sealed class NodeMenuAction {
|
||||
data class Remove(val node: NodeEntity) : NodeMenuAction()
|
||||
data class Ignore(val node: NodeEntity) : NodeMenuAction()
|
||||
data class DirectMessage(val node: NodeEntity) : NodeMenuAction()
|
||||
data class RequestUserInfo(val node: NodeEntity) : NodeMenuAction()
|
||||
data class RequestPosition(val node: NodeEntity) : NodeMenuAction()
|
||||
data class TraceRoute(val node: NodeEntity) : NodeMenuAction()
|
||||
data class MoreDetails(val node: NodeEntity) : NodeMenuAction()
|
||||
data class Remove(val node: Node) : NodeMenuAction()
|
||||
data class Ignore(val node: Node) : NodeMenuAction()
|
||||
data class DirectMessage(val node: Node) : NodeMenuAction()
|
||||
data class RequestUserInfo(val node: Node) : NodeMenuAction()
|
||||
data class RequestPosition(val node: Node) : NodeMenuAction()
|
||||
data class TraceRoute(val node: Node) : NodeMenuAction()
|
||||
data class MoreDetails(val node: Node) : NodeMenuAction()
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.ui.preview.NodePreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.theme.AppTheme
|
||||
|
||||
const val MAX_VALID_SNR = 100F
|
||||
|
@ -36,7 +36,7 @@ const val MAX_VALID_RSSI = 0
|
|||
@Composable
|
||||
fun SignalInfo(
|
||||
modifier: Modifier = Modifier,
|
||||
node: NodeEntity,
|
||||
node: Node,
|
||||
isThisNode: Boolean
|
||||
) {
|
||||
val text = if (isThisNode) {
|
||||
|
@ -81,7 +81,7 @@ fun SignalInfo(
|
|||
fun SignalInfoSimplePreview() {
|
||||
AppTheme {
|
||||
SignalInfo(
|
||||
node = NodeEntity(
|
||||
node = Node(
|
||||
num = 1,
|
||||
lastHeard = 0,
|
||||
channel = 0,
|
||||
|
@ -97,8 +97,8 @@ fun SignalInfoSimplePreview() {
|
|||
@PreviewLightDark
|
||||
@Composable
|
||||
fun SignalInfoPreview(
|
||||
@PreviewParameter(NodeEntityPreviewParameterProvider::class)
|
||||
node: NodeEntity
|
||||
@PreviewParameter(NodePreviewParameterProvider::class)
|
||||
node: Node
|
||||
) {
|
||||
AppTheme {
|
||||
SignalInfo(
|
||||
|
@ -111,8 +111,8 @@ fun SignalInfoPreview(
|
|||
@Composable
|
||||
@PreviewLightDark
|
||||
fun SignalInfoSelfPreview(
|
||||
@PreviewParameter(NodeEntityPreviewParameterProvider::class)
|
||||
node: NodeEntity
|
||||
@PreviewParameter(NodePreviewParameterProvider::class)
|
||||
node: Node
|
||||
) {
|
||||
AppTheme {
|
||||
SignalInfo(
|
||||
|
|
|
@ -81,6 +81,8 @@ fun NetworkConfigScreen(
|
|||
}
|
||||
|
||||
NetworkConfigItemList(
|
||||
hasWifi = state.metadata?.hasWifi ?: true,
|
||||
hasEthernet = state.metadata?.hasEthernet ?: true,
|
||||
networkConfig = state.radioConfig.network,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { networkInput ->
|
||||
|
@ -94,8 +96,11 @@ private fun extractWifiCredentials(qrCode: String) = Regex("""WIFI:S:(.*?);.*?P:
|
|||
.find(qrCode)?.destructured
|
||||
?.let { (ssid, password) -> ssid to password } ?: (null to null)
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun NetworkConfigItemList(
|
||||
hasWifi: Boolean,
|
||||
hasEthernet: Boolean,
|
||||
networkConfig: NetworkConfig,
|
||||
enabled: Boolean,
|
||||
onSaveClicked: (NetworkConfig) -> Unit,
|
||||
|
@ -137,16 +142,16 @@ fun NetworkConfigItemList(
|
|||
item {
|
||||
SwitchPreference(title = "WiFi enabled",
|
||||
checked = networkInput.wifiEnabled,
|
||||
enabled = enabled,
|
||||
enabled = enabled && hasWifi,
|
||||
onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } })
|
||||
Divider()
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "SSID",
|
||||
value = networkInput.wifiSsid,
|
||||
maxSize = 32, // wifi_ssid max_size:33
|
||||
enabled = enabled,
|
||||
enabled = enabled && hasWifi,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
|
@ -161,7 +166,7 @@ fun NetworkConfigItemList(
|
|||
EditPasswordPreference(title = "PSK",
|
||||
value = networkInput.wifiPsk,
|
||||
maxSize = 64, // wifi_psk max_size:65
|
||||
enabled = enabled,
|
||||
enabled = enabled && hasWifi,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } })
|
||||
}
|
||||
|
@ -173,12 +178,20 @@ fun NetworkConfigItemList(
|
|||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.height(48.dp),
|
||||
enabled = enabled,
|
||||
enabled = enabled && hasWifi,
|
||||
) {
|
||||
Text(text = stringResource(R.string.wifi_qr_code_scan))
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Ethernet enabled",
|
||||
checked = networkInput.ethEnabled,
|
||||
enabled = enabled && hasEthernet,
|
||||
onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } })
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "NTP server",
|
||||
value = networkInput.ntpServer,
|
||||
|
@ -209,14 +222,6 @@ fun NetworkConfigItemList(
|
|||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Ethernet enabled",
|
||||
checked = networkInput.ethEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "IPv4 mode",
|
||||
enabled = enabled,
|
||||
|
@ -225,8 +230,8 @@ fun NetworkConfigItemList(
|
|||
.map { it to it.name },
|
||||
selectedItem = networkInput.addressMode,
|
||||
onItemSelected = { networkInput = networkInput.copy { addressMode = it } })
|
||||
Divider()
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditIPv4Preference(title = "IP",
|
||||
|
@ -292,6 +297,8 @@ fun NetworkConfigItemList(
|
|||
@Composable
|
||||
private fun NetworkConfigPreview() {
|
||||
NetworkConfigItemList(
|
||||
hasWifi = true,
|
||||
hasEthernet = true,
|
||||
networkConfig = NetworkConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
onSaveClicked = { },
|
||||
|
|
|
@ -65,8 +65,8 @@ import com.geeksville.mesh.android.gpsDisabled
|
|||
import com.geeksville.mesh.android.hasGps
|
||||
import com.geeksville.mesh.android.hasLocationPermission
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.model.map.CustomTileSource
|
||||
import com.geeksville.mesh.model.map.MarkerWithLabel
|
||||
|
@ -311,7 +311,7 @@ fun MapView(
|
|||
AppCompatResources.getDrawable(context, R.drawable.ic_baseline_location_on_24)
|
||||
}
|
||||
|
||||
fun MapView.onNodesChanged(nodes: Collection<NodeEntity>): List<MarkerWithLabel> {
|
||||
fun MapView.onNodesChanged(nodes: Collection<Node>): List<MarkerWithLabel> {
|
||||
val nodesWithPosition = nodes.filter { it.validPosition != null }
|
||||
val ourNode = model.ourNodeInfo.value
|
||||
val gpsFormat = model.config.display.gpsFormat.number
|
||||
|
|
|
@ -90,8 +90,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.model.getChannel
|
||||
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
|
||||
|
@ -116,7 +116,7 @@ internal fun FragmentManager.navigateToMessages(contactKey: String, message: Str
|
|||
class MessagesFragment : Fragment(), Logging {
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
private fun navigateToMessages(node: NodeEntity) = node.user.let { user ->
|
||||
private fun navigateToMessages(node: Node) = node.user.let { user ->
|
||||
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC // TODO use meta.hasPKC
|
||||
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
|
||||
val contactKey = "$channel${user.id}"
|
||||
|
@ -168,7 +168,7 @@ internal fun MessageScreen(
|
|||
contactKey: String,
|
||||
message: String,
|
||||
viewModel: UIViewModel = hiltViewModel(),
|
||||
navigateToMessages: (NodeEntity) -> Unit,
|
||||
navigateToMessages: (Node) -> Unit,
|
||||
navigateToNodeDetails: (Int) -> Unit,
|
||||
onNavigateBack: () -> Unit
|
||||
) {
|
||||
|
|
|
@ -60,16 +60,16 @@ import androidx.compose.ui.unit.sp
|
|||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.ui.components.AutoLinkText
|
||||
import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.preview.NodePreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.theme.AppTheme
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun MessageItem(
|
||||
node: NodeEntity,
|
||||
node: Node,
|
||||
messageText: String?,
|
||||
messageTime: String,
|
||||
messageStatus: MessageStatus?,
|
||||
|
@ -197,7 +197,7 @@ internal fun MessageItem(
|
|||
private fun MessageItemPreview() {
|
||||
AppTheme {
|
||||
MessageItem(
|
||||
node = NodeEntityPreviewParameterProvider().values.first(),
|
||||
node = NodePreviewParameterProvider().values.first(),
|
||||
messageText = stringResource(R.string.sample_message),
|
||||
messageTime = "10:00",
|
||||
messageStatus = MessageStatus.DELIVERED,
|
||||
|
|
|
@ -20,19 +20,17 @@ package com.geeksville.mesh.ui.preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.geeksville.mesh.DeviceMetrics.Companion.currentTime
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.deviceMetrics
|
||||
import com.geeksville.mesh.environmentMetrics
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.paxcount
|
||||
import com.geeksville.mesh.position
|
||||
import com.geeksville.mesh.telemetry
|
||||
import com.geeksville.mesh.user
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlin.random.Random
|
||||
|
||||
class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity> {
|
||||
|
||||
val mickeyMouse = NodeEntity(
|
||||
class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
|
||||
val mickeyMouse = Node(
|
||||
num = 1955,
|
||||
user = user {
|
||||
id = "mickeyMouseId"
|
||||
|
@ -40,28 +38,22 @@ class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity>
|
|||
shortName = "MM"
|
||||
hwModel = MeshProtos.HardwareModel.TBEAM
|
||||
},
|
||||
longName = "Mickey Mouse",
|
||||
shortName = "MM",
|
||||
position = position {
|
||||
latitudeI = 338125110
|
||||
longitudeI = -1179189760
|
||||
altitude = 138
|
||||
satsInView = 4
|
||||
},
|
||||
latitude = 33.812511,
|
||||
longitude = -117.918976,
|
||||
lastHeard = currentTime(),
|
||||
channel = 0,
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
deviceTelemetry = telemetry {
|
||||
deviceMetrics = deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
}
|
||||
deviceMetrics = deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
},
|
||||
hopsAway = 0
|
||||
)
|
||||
|
@ -74,17 +66,13 @@ class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity>
|
|||
id = "minnieMouseId"
|
||||
hwModel = MeshProtos.HardwareModel.HELTEC_V3
|
||||
},
|
||||
longName = "Minnie Mouse",
|
||||
shortName = "MiMo",
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
position = position {},
|
||||
latitude = 0.0,
|
||||
longitude = 0.0,
|
||||
hopsAway = 1
|
||||
)
|
||||
|
||||
private val donaldDuck = NodeEntity(
|
||||
private val donaldDuck = Node(
|
||||
num = Random.nextInt(),
|
||||
position = position {
|
||||
latitudeI = 338052347
|
||||
|
@ -92,20 +80,16 @@ class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity>
|
|||
altitude = 121
|
||||
satsInView = 66
|
||||
},
|
||||
latitude = 33.8052347,
|
||||
longitude = -117.9208460,
|
||||
lastHeard = currentTime() - 300,
|
||||
channel = 0,
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
deviceTelemetry = telemetry {
|
||||
deviceMetrics = deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
}
|
||||
deviceMetrics = deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
},
|
||||
user = user {
|
||||
id = "donaldDuckId"
|
||||
|
@ -114,18 +98,14 @@ class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity>
|
|||
hwModel = MeshProtos.HardwareModel.HELTEC_V3
|
||||
publicKey = ByteString.copyFrom(ByteArray(32) { 1 })
|
||||
},
|
||||
longName = "Donald Duck, the Grand Duck of the Ducks",
|
||||
shortName = "DoDu",
|
||||
environmentTelemetry = telemetry {
|
||||
environmentMetrics = environmentMetrics {
|
||||
temperature = 28.0F
|
||||
relativeHumidity = 50.0F
|
||||
barometricPressure = 1013.25F
|
||||
gasResistance = 0.0F
|
||||
voltage = 3.7F
|
||||
current = 0.0F
|
||||
iaq = 100
|
||||
}
|
||||
environmentMetrics = environmentMetrics {
|
||||
temperature = 28.0F
|
||||
relativeHumidity = 50.0F
|
||||
barometricPressure = 1013.25F
|
||||
gasResistance = 0.0F
|
||||
voltage = 3.7F
|
||||
current = 0.0F
|
||||
iaq = 100
|
||||
},
|
||||
paxcounter = paxcount {
|
||||
wifi = 30
|
||||
|
@ -142,19 +122,15 @@ class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity>
|
|||
shortName = "myId"
|
||||
hwModel = MeshProtos.HardwareModel.UNSET
|
||||
},
|
||||
longName = "Meshtastic myId",
|
||||
shortName = null,
|
||||
environmentTelemetry = telemetry {
|
||||
environmentMetrics = environmentMetrics {}
|
||||
},
|
||||
environmentMetrics = environmentMetrics {},
|
||||
paxcounter = paxcount {},
|
||||
)
|
||||
|
||||
private val almostNothing = NodeEntity(
|
||||
private val almostNothing = Node(
|
||||
num = Random.nextInt(),
|
||||
)
|
||||
|
||||
override val values: Sequence<NodeEntity>
|
||||
override val values: Sequence<Node>
|
||||
get() = sequenceOf(
|
||||
mickeyMouse, // "this" node
|
||||
unknown,
|
Ładowanie…
Reference in New Issue