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 metadata
pull/1517/head
Andre K 2025-01-02 06:38:33 -03:00 zatwierdzone przez GitHub
rodzic bdefbc3ce2
commit 60e7e18116
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
28 zmienionych plików z 1164 dodań i 358 usunięć

Wyświetl plik

@ -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')"
]
}
}

Wyświetl plik

@ -24,8 +24,10 @@ import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.dao.NodeInfoDao import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.database.entity.MyNodeEntity import com.geeksville.mesh.database.entity.MyNodeEntity
import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.NodeSortOption import com.geeksville.mesh.model.NodeSortOption
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -40,6 +42,18 @@ class NodeInfoDaoTest {
private lateinit var database: MeshtasticDatabase private lateinit var database: MeshtasticDatabase
private lateinit var nodeInfoDao: NodeInfoDao 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( private val ourNode = NodeEntity(
num = 8, num = 8,
user = user { user = user {
@ -79,7 +93,7 @@ class NodeInfoDaoTest {
39.952583 to -75.165222, // Philadelphia 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( NodeEntity(
num = 9 + index, num = 9 + index,
user = user { user = user {
@ -89,7 +103,7 @@ class NodeInfoDaoTest {
hwModel = MeshProtos.HardwareModel.ANDROID_SIM hwModel = MeshProtos.HardwareModel.ANDROID_SIM
isLicensed = false 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, latitude = pos.first, longitude = pos.second,
lastHeard = 9 + index, lastHeard = 9 + index,
) )
@ -124,18 +138,18 @@ class NodeInfoDaoTest {
sort = sort.sqlValue, sort = sort.sqlValue,
filter = filter, filter = filter,
includeUnknown = includeUnknown, includeUnknown = includeUnknown,
).first().filter { it != ourNode } ).map { list -> list.map { it.toModel() } }.first().filter { it.num != ourNode.num }
@Test // node list size @Test // node list size
fun testNodeListSize() = runBlocking { fun testNodeListSize() = runBlocking {
val nodes = nodeInfoDao.nodeDBbyNum().first() 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 @Test // nodeDBbyNum() re-orders our node at the top of the list
fun testOurNodeInfoIsFirst() = runBlocking { fun testOurNodeInfoIsFirst() = runBlocking {
val nodes = nodeInfoDao.nodeDBbyNum().first() val nodes = nodeInfoDao.nodeDBbyNum().first()
assertEquals(ourNode, nodes.values.first()) assertEquals(ourNode.num, nodes.values.first().node.num)
} }
@Test @Test
@ -155,8 +169,9 @@ class NodeInfoDaoTest {
@Test @Test
fun testSortByDistance() = runBlocking { fun testSortByDistance() = runBlocking {
val nodes = getNodes(sort = NodeSortOption.DISTANCE) 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 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) assertEquals(sortedNodes, nodes)
} }
@ -185,7 +200,7 @@ class NodeInfoDaoTest {
@Test @Test
fun testIncludeUnknownIsTrue() = runBlocking { fun testIncludeUnknownIsTrue() = runBlocking {
val nodes = getNodes(includeUnknown = true) val nodes = getNodes(includeUnknown = true)
val containsUnsetNode = nodes.any { it.shortName == null } val containsUnsetNode = nodes.any { it.isUnknownUser }
assertTrue(containsUnsetNode) assertTrue(containsUnsetNode)
} }
} }

Wyświetl plik

@ -114,4 +114,19 @@ class Converters : Logging {
fun paxCounterToBytes(value: PaxcountProtos.Paxcount): ByteArray? { fun paxCounterToBytes(value: PaxcountProtos.Paxcount): ByteArray? {
return value.toByteArray() 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()
}
} }

Wyświetl plik

@ -31,6 +31,7 @@ import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.database.dao.QuickChatActionDao import com.geeksville.mesh.database.dao.QuickChatActionDao
import com.geeksville.mesh.database.entity.ContactSettings import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.MeshLog 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.MyNodeEntity
import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.Packet
@ -46,6 +47,7 @@ import com.geeksville.mesh.database.entity.ReactionEntity
MeshLog::class, MeshLog::class,
QuickChatAction::class, QuickChatAction::class,
ReactionEntity::class, ReactionEntity::class,
MetadataEntity::class,
], ],
autoMigrations = [ autoMigrations = [
AutoMigration(from = 3, to = 4), 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 = 12, to = 13, spec = AutoMigration12to13::class),
AutoMigration(from = 13, to = 14), AutoMigration(from = 13, to = 14),
AutoMigration(from = 14, to = 15), AutoMigration(from = 14, to = 15),
AutoMigration(from = 15, to = 16),
], ],
version = 15, version = 16,
exportSchema = true, exportSchema = true,
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)

Wyświetl plik

@ -23,14 +23,19 @@ import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.DataPacket import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.database.dao.NodeInfoDao 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.MyNodeEntity
import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.NodeSortOption import com.geeksville.mesh.model.NodeSortOption
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -49,15 +54,20 @@ class NodeRepository @Inject constructor(
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, null) .stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, null)
// our node info // our node info
private val _ourNodeInfo = MutableStateFlow<NodeEntity?>(null) private val _ourNodeInfo = MutableStateFlow<Node?>(null)
val ourNodeInfo: StateFlow<NodeEntity?> get() = _ourNodeInfo val ourNodeInfo: StateFlow<Node?> get() = _ourNodeInfo
// The unique userId of our node // The unique userId of our node
private val _myId = MutableStateFlow<String?>(null) private val _myId = MutableStateFlow<String?>(null)
val myId: StateFlow<String?> get() = _myId val myId: StateFlow<String?> get() = _myId
// A map from nodeNum to NodeEntity fun getNodeDBbyNum() = nodeInfoDao.nodeDBbyNum()
val nodeDBbyNum: StateFlow<Map<Int, NodeEntity>> = 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 { .onEach {
val ourNodeInfo = it.values.firstOrNull() val ourNodeInfo = it.values.firstOrNull()
_ourNodeInfo.value = ourNodeInfo _ourNodeInfo.value = ourNodeInfo
@ -67,8 +77,8 @@ class NodeRepository @Inject constructor(
.conflate() .conflate()
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap()) .stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
fun getNode(userId: String): NodeEntity = nodeDBbyNum.value.values.find { it.user.id == userId } fun getNode(userId: String): Node = nodeDBbyNum.value.values.find { it.user.id == userId }
?: NodeEntity( ?: Node(
num = DataPacket.idToDefaultNodeNum(userId) ?: 0, num = DataPacket.idToDefaultNodeNum(userId) ?: 0,
user = getUser(userId), user = getUser(userId),
) )
@ -84,6 +94,7 @@ class NodeRepository @Inject constructor(
.setHwModel(MeshProtos.HardwareModel.UNSET) .setHwModel(MeshProtos.HardwareModel.UNSET)
.build() .build()
@OptIn(ExperimentalCoroutinesApi::class)
fun getNodes( fun getNodes(
sort: NodeSortOption = NodeSortOption.LAST_HEARD, sort: NodeSortOption = NodeSortOption.LAST_HEARD,
filter: String = "", filter: String = "",
@ -92,7 +103,7 @@ class NodeRepository @Inject constructor(
sort = sort.sqlValue, sort = sort.sqlValue,
filter = filter, filter = filter,
includeUnknown = includeUnknown, includeUnknown = includeUnknown,
).flowOn(dispatchers.io).conflate() ).mapLatest { list -> list.map { it.toModel() } }.flowOn(dispatchers.io).conflate()
suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) { suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) {
nodeInfoDao.upsert(node) nodeInfoDao.upsert(node)
@ -107,5 +118,10 @@ class NodeRepository @Inject constructor(
suspend fun deleteNode(num: Int) = withContext(dispatchers.io) { suspend fun deleteNode(num: Int) = withContext(dispatchers.io) {
nodeInfoDao.deleteNode(num) nodeInfoDao.deleteNode(num)
nodeInfoDao.deleteMetadata(num)
}
suspend fun insertMetadata(metadata: MetadataEntity) = withContext(dispatchers.io) {
nodeInfoDao.upsert(metadata)
} }
} }

Wyświetl plik

@ -22,11 +22,15 @@ import androidx.room.Insert
import androidx.room.MapColumn import androidx.room.MapColumn
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert import androidx.room.Upsert
import com.geeksville.mesh.database.entity.MetadataEntity
import com.geeksville.mesh.database.entity.MyNodeEntity import com.geeksville.mesh.database.entity.MyNodeEntity
import com.geeksville.mesh.database.entity.NodeWithRelations
import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.database.entity.NodeEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Suppress("TooManyFunctions")
@Dao @Dao
interface NodeInfoDao { interface NodeInfoDao {
@ -49,7 +53,8 @@ interface NodeInfoDao {
last_heard DESC last_heard DESC
""" """
) )
fun nodeDBbyNum(): Flow<Map<@MapColumn(columnName = "num") Int, NodeEntity>> @Transaction
fun nodeDBbyNum(): Flow<Map<@MapColumn(columnName = "num") Int, NodeWithRelations>>
@Query( @Query(
""" """
@ -92,11 +97,12 @@ interface NodeInfoDao {
last_heard DESC last_heard DESC
""" """
) )
@Transaction
fun getNodes( fun getNodes(
sort: String, sort: String,
filter: String, filter: String,
includeUnknown: Boolean, includeUnknown: Boolean,
): Flow<List<NodeEntity>> ): Flow<List<NodeWithRelations>>
@Upsert @Upsert
fun upsert(node: NodeEntity) fun upsert(node: NodeEntity)
@ -109,4 +115,10 @@ interface NodeInfoDao {
@Query("DELETE FROM nodes WHERE num=:num") @Query("DELETE FROM nodes WHERE num=:num")
fun deleteNode(num: Int) fun deleteNode(num: Int)
@Upsert
fun upsert(meta: MetadataEntity)
@Query("DELETE FROM metadata WHERE num=:num")
fun deleteMetadata(num: Int)
} }

Wyświetl plik

@ -17,11 +17,12 @@
package com.geeksville.mesh.database.entity package com.geeksville.mesh.database.entity
import android.graphics.Color
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig import androidx.room.Relation
import com.geeksville.mesh.DeviceMetrics import com.geeksville.mesh.DeviceMetrics
import com.geeksville.mesh.EnvironmentMetrics import com.geeksville.mesh.EnvironmentMetrics
import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.MeshProtos
@ -31,16 +32,72 @@ import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.Position import com.geeksville.mesh.Position
import com.geeksville.mesh.TelemetryProtos import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.copy import com.geeksville.mesh.copy
import com.geeksville.mesh.util.bearing import com.geeksville.mesh.model.Node
import com.geeksville.mesh.util.GPSFormat
import com.geeksville.mesh.util.latLongToMeter
import com.geeksville.mesh.util.toDistanceString
import com.google.protobuf.ByteString 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") @Suppress("MagicNumber")
@Entity(tableName = "nodes") @Entity(tableName = "nodes")
data class NodeEntity( data class NodeEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
val num: Int, // This is immutable, and used as a key val num: Int, // This is immutable, and used as a key
@ -92,32 +149,9 @@ data class NodeEntity(
val environmentMetrics: TelemetryProtos.EnvironmentMetrics val environmentMetrics: TelemetryProtos.EnvironmentMetrics
get() = environmentTelemetry.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 isUnknownUser get() = user.hwModel == MeshProtos.HardwareModel.UNSET
val hasPKC get() = !user.publicKey.isEmpty val hasPKC get() = !user.publicKey.isEmpty
val errorByteString: ByteString get() = ByteString.copyFrom(ByteArray(32) { 0 }) 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()) { fun setPosition(p: MeshProtos.Position, defaultTime: Int = currentTime()) {
position = p.copy { time = if (p.time != 0) p.time else defaultTime } position = p.copy { time = if (p.time != 0) p.time else defaultTime }
@ -125,75 +159,6 @@ data class NodeEntity(
longitude = degD(p.longitudeI) 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 * true if the device was heard from recently
*/ */
@ -211,48 +176,48 @@ data class NodeEntity(
fun currentTime() = (System.currentTimeMillis() / 1000).toInt() fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
} }
}
fun NodeEntity.toNodeInfo() = NodeInfo( fun toNodeInfo() = NodeInfo(
num = num, num = num,
user = MeshUser( user = MeshUser(
id = user.id, id = user.id,
longName = user.longName, longName = user.longName,
shortName = user.shortName, shortName = user.shortName,
hwModel = user.hwModel, hwModel = user.hwModel,
role = user.roleValue, role = user.roleValue,
).takeIf { user.id.isNotEmpty() }, ).takeIf { user.id.isNotEmpty() },
position = Position( position = Position(
latitude = latitude, latitude = latitude,
longitude = longitude, longitude = longitude,
altitude = position.altitude, altitude = position.altitude,
time = position.time, time = position.time,
satellitesInView = position.satsInView, satellitesInView = position.satsInView,
groundSpeed = position.groundSpeed, groundSpeed = position.groundSpeed,
groundTrack = position.groundTrack, groundTrack = position.groundTrack,
precisionBits = position.precisionBits, precisionBits = position.precisionBits,
).takeIf { it.isValid() }, ).takeIf { it.isValid() },
snr = snr, snr = snr,
rssi = rssi, rssi = rssi,
lastHeard = lastHeard, lastHeard = lastHeard,
deviceMetrics = DeviceMetrics( deviceMetrics = DeviceMetrics(
time = deviceTelemetry.time, time = deviceTelemetry.time,
batteryLevel = deviceMetrics.batteryLevel, batteryLevel = deviceMetrics.batteryLevel,
voltage = deviceMetrics.voltage, voltage = deviceMetrics.voltage,
channelUtilization = deviceMetrics.channelUtilization, channelUtilization = deviceMetrics.channelUtilization,
airUtilTx = deviceMetrics.airUtilTx, airUtilTx = deviceMetrics.airUtilTx,
uptimeSeconds = deviceMetrics.uptimeSeconds, uptimeSeconds = deviceMetrics.uptimeSeconds,
), ),
channel = channel, channel = channel,
environmentMetrics = EnvironmentMetrics( environmentMetrics = EnvironmentMetrics(
time = environmentTelemetry.time, time = environmentTelemetry.time,
temperature = environmentMetrics.temperature, temperature = environmentMetrics.temperature,
relativeHumidity = environmentMetrics.relativeHumidity, relativeHumidity = environmentMetrics.relativeHumidity,
barometricPressure = environmentMetrics.barometricPressure, barometricPressure = environmentMetrics.barometricPressure,
gasResistance = environmentMetrics.gasResistance, gasResistance = environmentMetrics.gasResistance,
voltage = environmentMetrics.voltage, voltage = environmentMetrics.voltage,
current = environmentMetrics.current, current = environmentMetrics.current,
iaq = environmentMetrics.iaq, iaq = environmentMetrics.iaq,
), ),
hopsAway = hopsAway, hopsAway = hopsAway,
) )
}

Wyświetl plik

@ -26,6 +26,7 @@ import androidx.room.Relation
import com.geeksville.mesh.DataPacket import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MeshProtos.User import com.geeksville.mesh.MeshProtos.User
import com.geeksville.mesh.model.Message import com.geeksville.mesh.model.Message
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.util.getShortDateTime import com.geeksville.mesh.util.getShortDateTime
data class PacketEntity( data class PacketEntity(
@ -33,7 +34,7 @@ data class PacketEntity(
@Relation(entity = ReactionEntity::class, parentColumn = "packet_id", entityColumn = "reply_id") @Relation(entity = ReactionEntity::class, parentColumn = "packet_id", entityColumn = "reply_id")
val reactions: List<ReactionEntity> = emptyList(), 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( Message(
uuid = uuid, uuid = uuid,
receivedTime = received_time, receivedTime = received_time,
@ -101,7 +102,7 @@ data class ReactionEntity(
) )
private suspend fun ReactionEntity.toReaction( private suspend fun ReactionEntity.toReaction(
getNode: suspend (userId: String?) -> NodeEntity getNode: suspend (userId: String?) -> Node
) = Reaction( ) = Reaction(
replyId = replyId, replyId = replyId,
user = getNode(userId).user, user = getNode(userId).user,
@ -110,5 +111,5 @@ private suspend fun ReactionEntity.toReaction(
) )
private suspend fun List<ReactionEntity>.toReaction( private suspend fun List<ReactionEntity>.toReaction(
getNode: suspend (userId: String?) -> NodeEntity getNode: suspend (userId: String?) -> Node
) = this.map { it.toReaction(getNode) } ) = this.map { it.toReaction(getNode) }

Wyświetl plik

@ -21,7 +21,6 @@ import androidx.annotation.StringRes
import com.geeksville.mesh.MeshProtos.Routing import com.geeksville.mesh.MeshProtos.Routing
import com.geeksville.mesh.MessageStatus import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.Reaction import com.geeksville.mesh.database.entity.Reaction
@Suppress("CyclomaticComplexMethod") @Suppress("CyclomaticComplexMethod")
@ -49,7 +48,7 @@ fun getStringResFrom(routingError: Int): Int = when (routingError) {
data class Message( data class Message(
val uuid: Long, val uuid: Long,
val receivedTime: Long, val receivedTime: Long,
val node: NodeEntity, val node: Node,
val text: String, val text: String,
val time: String, val time: String,
val read: Boolean, val read: Boolean,

Wyświetl plik

@ -38,7 +38,6 @@ import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.MeshLogRepository import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.entity.MeshLog 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.model.map.CustomTileSource
import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.ui.Route import com.geeksville.mesh.ui.Route
@ -71,7 +70,7 @@ data class MetricsState(
val isManaged: Boolean = true, val isManaged: Boolean = true,
val isFahrenheit: Boolean = false, val isFahrenheit: Boolean = false,
val displayUnits: DisplayUnits = DisplayUnits.METRIC, val displayUnits: DisplayUnits = DisplayUnits.METRIC,
val node: NodeEntity? = null, val node: Node? = null,
val deviceMetrics: List<Telemetry> = emptyList(), val deviceMetrics: List<Telemetry> = emptyList(),
val environmentMetrics: List<Telemetry> = emptyList(), val environmentMetrics: List<Telemetry> = emptyList(),
val signalMetrics: List<MeshPacket> = emptyList(), val signalMetrics: List<MeshPacket> = emptyList(),

Wyświetl plik

@ -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(" ")
}
}

Wyświetl plik

@ -38,7 +38,6 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.config import com.geeksville.mesh.config
import com.geeksville.mesh.database.entity.MyNodeEntity import com.geeksville.mesh.database.entity.MyNodeEntity
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.deviceProfile import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.datastore.RadioConfigRepository
@ -56,6 +55,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
@ -73,7 +73,7 @@ data class RadioConfigState(
val isLocal: Boolean = false, val isLocal: Boolean = false,
val connected: Boolean = false, val connected: Boolean = false,
val route: String = "", val route: String = "",
val metadata: MeshProtos.DeviceMetadata = MeshProtos.DeviceMetadata.getDefaultInstance(), val metadata: MeshProtos.DeviceMetadata? = null,
val userConfig: MeshProtos.User = MeshProtos.User.getDefaultInstance(), val userConfig: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
val channelList: List<ChannelProtos.ChannelSettings> = emptyList(), val channelList: List<ChannelProtos.ChannelSettings> = emptyList(),
val radioConfig: ConfigProtos.Config = config {}, val radioConfig: ConfigProtos.Config = config {},
@ -81,9 +81,7 @@ data class RadioConfigState(
val ringtone: String = "", val ringtone: String = "",
val cannedMessageMessages: String = "", val cannedMessageMessages: String = "",
val responseState: ResponseState<Boolean> = ResponseState.Empty, val responseState: ResponseState<Boolean> = ResponseState.Empty,
) { )
fun hasMetadata() = metadata != MeshProtos.DeviceMetadata.getDefaultInstance()
}
@HiltViewModel @HiltViewModel
class RadioConfigViewModel @Inject constructor( class RadioConfigViewModel @Inject constructor(
@ -94,8 +92,8 @@ class RadioConfigViewModel @Inject constructor(
private val meshService: IMeshService? get() = radioConfigRepository.meshService private val meshService: IMeshService? get() = radioConfigRepository.meshService
private val destNum = savedStateHandle.toRoute<Route.RadioConfig>().destNum private val destNum = savedStateHandle.toRoute<Route.RadioConfig>().destNum
private val _destNode = MutableStateFlow<NodeEntity?>(null) private val _destNode = MutableStateFlow<Node?>(null)
val destNode: StateFlow<NodeEntity?> get() = _destNode val destNode: StateFlow<Node?> get() = _destNode
private val requestIds = MutableStateFlow(hashSetOf<Int>()) private val requestIds = MutableStateFlow(hashSetOf<Int>())
private val _radioConfigState = MutableStateFlow(RadioConfigState()) private val _radioConfigState = MutableStateFlow(RadioConfigState())
@ -106,9 +104,14 @@ class RadioConfigViewModel @Inject constructor(
init { init {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
radioConfigRepository.nodeDBbyNum.mapLatest { nodes -> radioConfigRepository.nodeDBbyNum
nodes[destNum] ?: nodes.values.firstOrNull() .mapLatest { nodes -> nodes[destNum] ?: nodes.values.firstOrNull() }
}.onEach { _destNode.value = it }.launchIn(viewModelScope) .distinctUntilChanged()
.onEach {
_destNode.value = it
_radioConfigState.update { state -> state.copy(metadata = it?.metadata) }
}
.launchIn(viewModelScope)
radioConfigRepository.deviceProfileFlow.onEach { radioConfigRepository.deviceProfileFlow.onEach {
_currentDeviceProfile.value = it _currentDeviceProfile.value = it
@ -322,7 +325,7 @@ class RadioConfigViewModel @Inject constructor(
when (route) { when (route) {
AdminRoute.REBOOT.name -> requestReboot(destNum) AdminRoute.REBOOT.name -> requestReboot(destNum)
AdminRoute.SHUTDOWN.name -> with(radioConfigState.value) { AdminRoute.SHUTDOWN.name -> with(radioConfigState.value) {
if (hasMetadata() && !metadata.canShutdown) { if (metadata != null && !metadata.canShutdown) {
sendError(R.string.cant_shutdown) sendError(R.string.cant_shutdown)
} else { } else {
requestShutdown(destNum) 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) { fun setFixedPosition(position: Position) {
val destNum = destNode.value?.num ?: return val destNum = destNode.value?.num ?: return
try { try {
@ -441,10 +435,15 @@ class RadioConfigViewModel @Inject constructor(
fun setResponseStateLoading(route: Enum<*>) { fun setResponseStateLoading(route: Enum<*>) {
val destNum = destNode.value?.num ?: return val destNum = destNode.value?.num ?: return
_radioConfigState.value = RadioConfigState( _radioConfigState.update {
route = route.name, RadioConfigState(
responseState = ResponseState.Loading(), isLocal = it.isLocal,
) connected = it.connected,
route = route.name,
metadata = it.metadata,
responseState = ResponseState.Loading(),
)
}
when (route) { when (route) {
ConfigRoute.USER -> getOwner(destNum) ConfigRoute.USER -> getOwner(destNum)
@ -456,7 +455,10 @@ class RadioConfigViewModel @Inject constructor(
setResponseStateTotal(maxChannels + 1) setResponseStateTotal(maxChannels + 1)
} }
is AdminRoute -> getSessionPasskey(destNum) is AdminRoute -> {
getConfig(destNum, AdminProtos.AdminMessage.ConfigType.SESSIONKEY_CONFIG_VALUE)
setResponseStateTotal(2)
}
is ConfigRoute -> { is ConfigRoute -> {
if (route == ConfigRoute.LORA) { if (route == ConfigRoute.LORA) {

Wyświetl plik

@ -52,7 +52,6 @@ import com.geeksville.mesh.database.NodeRepository
import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.QuickChatActionRepository import com.geeksville.mesh.database.QuickChatActionRepository
import com.geeksville.mesh.database.entity.MyNodeEntity 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.Packet
import com.geeksville.mesh.database.entity.QuickChatAction import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.datastore.RadioConfigRepository
@ -235,7 +234,7 @@ class UIViewModel @Inject constructor(
) )
@OptIn(ExperimentalCoroutinesApi::class) @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) nodeDB.getNodes(state.sort, state.filter, state.includeUnknown)
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
@ -245,7 +244,7 @@ class UIViewModel @Inject constructor(
// hardware info about our local device (can be null) // hardware info about our local device (can be null)
val myNodeInfo: StateFlow<MyNodeEntity?> get() = nodeDB.myNodeInfo 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 } val nodesWithPosition get() = nodeDB.nodeDBbyNum.value.values.filter { it.validPosition != null }
@ -484,7 +483,7 @@ class UIViewModel @Inject constructor(
updateLoraConfig { it.copy { region = value } } updateLoraConfig { it.copy { region = value } }
} }
fun ignoreNode(node: NodeEntity) = viewModelScope.launch { fun ignoreNode(node: Node) = viewModelScope.launch {
try { try {
radioConfigRepository.onServiceAction(ServiceAction.Ignore(node)) radioConfigRepository.onServiceAction(ServiceAction.Ignore(node))
} catch (ex: RemoteException) { } catch (ex: RemoteException) {

Wyświetl plik

@ -25,12 +25,15 @@ import com.geeksville.mesh.ConfigProtos.Config
import com.geeksville.mesh.IMeshService import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig 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.MyNodeEntity
import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.deviceProfile 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.model.getChannelUrl
import com.geeksville.mesh.service.MeshService.ConnectionState import com.geeksville.mesh.service.MeshService.ConnectionState
import com.geeksville.mesh.service.ServiceAction import com.geeksville.mesh.service.ServiceAction
@ -40,6 +43,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -70,16 +74,20 @@ class RadioConfigRepository @Inject constructor(
val myNodeInfo: StateFlow<MyNodeEntity?> get() = nodeDB.myNodeInfo 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) fun getUser(nodeNum: Int) = nodeDB.getUser(nodeNum)
suspend fun getNodeDBbyNum() = nodeDB.getNodeDBbyNum().first()
suspend fun upsert(node: NodeEntity) = nodeDB.upsert(node) suspend fun upsert(node: NodeEntity) = nodeDB.upsert(node)
suspend fun installNodeDB(mi: MyNodeEntity, nodes: List<NodeEntity>) { suspend fun installNodeDB(mi: MyNodeEntity, nodes: List<NodeEntity>) {
nodeDB.installNodeDB(mi, nodes) nodeDB.installNodeDB(mi, nodes)
} }
suspend fun insertMetadata(fromNum: Int, metadata: DeviceMetadata) {
nodeDB.insertMetadata(MetadataEntity(fromNum, metadata))
}
/** /**
* Flow representing the [ChannelSet] data store. * Flow representing the [ChannelSet] data store.
@ -195,7 +203,7 @@ class RadioConfigRepository @Inject constructor(
serviceRepository.emitMeshPacket(packet) serviceRepository.emitMeshPacket(packet)
} }
val serviceAction: SharedFlow<ServiceAction> get() = serviceRepository.serviceAction val serviceAction: Flow<ServiceAction> get() = serviceRepository.serviceAction
suspend fun onServiceAction(action: ServiceAction) = coroutineScope { suspend fun onServiceAction(action: ServiceAction) = coroutineScope {
serviceRepository.onServiceAction(action) serviceRepository.onServiceAction(action)

Wyświetl plik

@ -26,17 +26,38 @@ import android.os.IBinder
import android.os.RemoteException import android.os.RemoteException
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.core.location.LocationCompat 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.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio 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.TelemetryProtos.LocalStats
import com.geeksville.mesh.analytics.DataPair import com.geeksville.mesh.analytics.DataPair
import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hasLocationPermission import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.concurrent.handledLaunch 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.MeshLogRepository
import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.MeshLog 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.NodeEntity
import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.ReactionEntity 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.DeviceVersion
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.getTracerouteResponse import com.geeksville.mesh.model.getTracerouteResponse
import com.geeksville.mesh.position
import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.repository.network.MQTTRepository import com.geeksville.mesh.repository.network.MQTTRepository
import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.radio.RadioServiceConnectionState 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.ByteString
import com.google.protobuf.InvalidProtocolBufferException import com.google.protobuf.InvalidProtocolBufferException
import dagger.Lazy import dagger.Lazy
@ -77,7 +105,8 @@ import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
sealed class ServiceAction { 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() data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction()
} }
@ -302,12 +331,8 @@ class MeshService : Service(), Logging {
.launchIn(serviceScope) .launchIn(serviceScope)
radioConfigRepository.channelSetFlow.onEach { channelSet = it } radioConfigRepository.channelSetFlow.onEach { channelSet = it }
.launchIn(serviceScope) .launchIn(serviceScope)
radioConfigRepository.serviceAction.onEach { action -> radioConfigRepository.serviceAction.onEach(::onServiceAction)
when (action) { .launchIn(serviceScope)
is ServiceAction.Ignore -> ignoreNode(action.node)
is ServiceAction.Reaction -> sendReaction(action)
}
}.launchIn(serviceScope)
loadSettings() // Load our last known node DB loadSettings() // Load our last known node DB
@ -375,10 +400,10 @@ class MeshService : Service(), Logging {
// BEGINNING OF MODEL - FIXME, move elsewhere // BEGINNING OF MODEL - FIXME, move elsewhere
// //
private fun loadSettings() { private fun loadSettings() = serviceScope.handledLaunch {
discardNodeDB() // Get rid of any old state discardNodeDB() // Get rid of any old state
myNodeInfo = radioConfigRepository.myNodeInfo.value 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) // 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) { private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage) {
if (fromNodeNum == myNodeNum) { when (a.payloadVariantCase) {
when (a.payloadVariantCase) { AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> {
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> { if (fromNodeNum == myNodeNum) {
val response = a.getConfigResponse val response = a.getConfigResponse
debug("Admin: received config ${response.payloadVariantCase}") debug("Admin: received config ${response.payloadVariantCase}")
setLocalConfig(response) setLocalConfig(response)
} }
}
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> { AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
if (fromNodeNum == myNodeNum) {
val mi = myNodeInfo val mi = myNodeInfo
if (mi != null) { if (mi != null) {
val ch = a.getChannelResponse 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") AdminProtos.AdminMessage.PayloadVariantCase.GET_DEVICE_METADATA_RESPONSE -> {
sessionPasskey = a.sessionPasskey 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 // 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 { private fun updateChannelSettings(ch: ChannelProtos.Channel) = serviceScope.handledLaunch {
radioConfigRepository.updateChannelSettings(ch) radioConfigRepository.updateChannelSettings(ch)
} }
@ -1476,31 +1503,33 @@ class MeshService : Service(), Logging {
} }
private var rawMyNodeInfo: MeshProtos.MyNodeInfo? = null 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 /** 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. * 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 val myInfo = rawMyNodeInfo
if (myInfo != null) { if (myInfo != null) {
val mi = with(myInfo) { val mi = with(myInfo) {
MyNodeEntity( MyNodeEntity(
myNodeNum = myNodeNum, myNodeNum = myNodeNum,
model = when (val hwModel = rawDeviceMetadata?.hwModel) { model = when (val hwModel = metadata.hwModel) {
null, MeshProtos.HardwareModel.UNSET -> null null, MeshProtos.HardwareModel.UNSET -> null
else -> hwModel.name.replace('_', '-').replace('p', '.').lowercase() else -> hwModel.name.replace('_', '-').replace('p', '.').lowercase()
}, },
firmwareVersion = rawDeviceMetadata?.firmwareVersion, firmwareVersion = metadata.firmwareVersion,
couldUpdate = false, couldUpdate = false,
shouldUpdate = false, // TODO add check after re-implementing firmware updates shouldUpdate = false, // TODO add check after re-implementing firmware updates
currentPacketId = currentPacketId and 0xffffffffL, currentPacketId = currentPacketId and 0xffffffffL,
messageTimeoutMsec = 5 * 60 * 1000, // constants from current firmware code messageTimeoutMsec = 5 * 60 * 1000, // constants from current firmware code
minAppVersion = minAppVersion, minAppVersion = minAppVersion,
maxChannels = 8, maxChannels = 8,
hasWifi = rawDeviceMetadata?.hasWifi ?: false, hasWifi = metadata.hasWifi,
) )
} }
serviceScope.handledLaunch {
radioConfigRepository.insertMetadata(mi.myNodeNum, metadata)
}
newMyNodeInfo = mi newMyNodeInfo = mi
} }
} }
@ -1554,8 +1583,7 @@ class MeshService : Service(), Logging {
) )
insertMeshLog(packetToSave) insertMeshLog(packetToSave)
rawDeviceMetadata = metadata regenMyNodeInfo(metadata)
regenMyNodeInfo()
} }
/** /**
@ -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 { sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
if (node.isIgnored) { if (node.isIgnored) {
debug("removing node ${node.num} from ignore list") debug("removing node ${node.num} from ignore list")

Wyświetl plik

@ -20,10 +20,12 @@ package com.geeksville.mesh.service
import com.geeksville.mesh.IMeshService import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -86,10 +88,10 @@ class ServiceRepository @Inject constructor() : Logging {
setTracerouteResponse(null) setTracerouteResponse(null)
} }
private val _serviceAction = MutableSharedFlow<ServiceAction>() private val _serviceAction = Channel<ServiceAction>()
val serviceAction: SharedFlow<ServiceAction> get() = _serviceAction val serviceAction = _serviceAction.receiveAsFlow()
suspend fun onServiceAction(action: ServiceAction) { suspend fun onServiceAction(action: ServiceAction) {
_serviceAction.emit(action) _serviceAction.send(action)
} }
} }

Wyświetl plik

@ -71,6 +71,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.MetricsViewModel 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), LORA("LoRa", Route.LoRa, Icons.Default.CellTower, 5),
BLUETOOTH("Bluetooth", Route.Bluetooth, Icons.Default.Bluetooth, 6), BLUETOOTH("Bluetooth", Route.Bluetooth, Icons.Default.Bluetooth, 6),
SECURITY("Security", Route.Security, Icons.Default.Security, type = 7), 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) // 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), AMBIENT_LIGHTING("Ambient Lighting", Route.AmbientLighting, Icons.Default.LightMode, 10),
DETECTION_SENSOR("Detection Sensor", Route.DetectionSensor, Icons.Default.Sensors, 11), DETECTION_SENSOR("Detection Sensor", Route.DetectionSensor, Icons.Default.Sensors, 11),
PAXCOUNTER("Paxcounter", Route.Paxcounter, Icons.Default.PermScanWifi, 12), 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
}
}
}
} }
/** /**

Wyświetl plik

@ -58,6 +58,7 @@ import androidx.compose.material.icons.filled.KeyOff
import androidx.compose.material.icons.filled.LightMode import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Map 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.Numbers
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Power import androidx.compose.material.icons.filled.Power
@ -91,11 +92,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.model.MetricsState import com.geeksville.mesh.model.MetricsState
import com.geeksville.mesh.model.MetricsViewModel import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.ui.components.PreferenceCategory 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.ui.theme.AppTheme
import com.geeksville.mesh.util.DistanceUnit import com.geeksville.mesh.util.DistanceUnit
import com.geeksville.mesh.util.formatAgo import com.geeksville.mesh.util.formatAgo
@ -132,7 +133,7 @@ fun NodeDetailScreen(
@Composable @Composable
private fun NodeDetailList( private fun NodeDetailList(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
node: NodeEntity, node: Node,
metricsState: MetricsState, metricsState: MetricsState,
onNavigate: (Any) -> Unit = {}, onNavigate: (Any) -> Unit = {},
) { ) {
@ -257,7 +258,7 @@ private fun DeviceDetailsContent(
@Composable @Composable
private fun NodeDetailsContent( private fun NodeDetailsContent(
node: NodeEntity, node: Node,
) { ) {
if (node.mismatchKey) { if (node.mismatchKey) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@ -304,6 +305,13 @@ private fun NodeDetailsContent(
value = formatUptime(node.deviceMetrics.uptimeSeconds) value = formatUptime(node.deviceMetrics.uptimeSeconds)
) )
} }
if (node.metadata != null) {
NodeDetailRow(
label = "Firmware version",
icon = Icons.Default.Memory,
value = node.metadata.firmwareVersion.substringBeforeLast(".")
)
}
NodeDetailRow( NodeDetailRow(
label = "Last heard", label = "Last heard",
icon = Icons.Default.History, icon = Icons.Default.History,
@ -413,7 +421,7 @@ private fun InfoCard(
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable @Composable
private fun EnvironmentMetrics( private fun EnvironmentMetrics(
node: NodeEntity, node: Node,
isFahrenheit: Boolean = false, isFahrenheit: Boolean = false,
) = with(node.environmentMetrics) { ) = with(node.environmentMetrics) {
FlowRow( FlowRow(
@ -543,7 +551,7 @@ private fun calculateDewPoint(tempCelsius: Float, humidity: Float): Float {
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun PowerMetrics(node: NodeEntity) = with(node.powerMetrics) { private fun PowerMetrics(node: Node) = with(node.powerMetrics) {
FlowRow( FlowRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@ -597,8 +605,8 @@ private fun PowerMetrics(node: NodeEntity) = with(node.powerMetrics) {
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun NodeDetailsPreview( private fun NodeDetailsPreview(
@PreviewParameter(NodeEntityPreviewParameterProvider::class) @PreviewParameter(NodePreviewParameterProvider::class)
node: NodeEntity node: Node
) { ) {
AppTheme { AppTheme {
NodeDetailList( NodeDetailList(

Wyświetl plik

@ -59,14 +59,14 @@ import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.model.Node
import com.geeksville.mesh.ui.components.NodeMenuAction
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
import com.geeksville.mesh.ui.components.NodeMenu 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.components.SignalInfo
import com.geeksville.mesh.ui.compose.ElevationInfo import com.geeksville.mesh.ui.compose.ElevationInfo
import com.geeksville.mesh.ui.compose.SatelliteCountInfo 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.ui.theme.AppTheme
import com.geeksville.mesh.util.toDistanceString import com.geeksville.mesh.util.toDistanceString
@ -74,8 +74,8 @@ import com.geeksville.mesh.util.toDistanceString
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun NodeItem( fun NodeItem(
thisNode: NodeEntity?, thisNode: Node?,
thatNode: NodeEntity, thatNode: Node,
gpsFormat: Int, gpsFormat: Int,
distanceUnits: Int, distanceUnits: Int,
tempInFahrenheit: Boolean, tempInFahrenheit: Boolean,
@ -293,8 +293,8 @@ fun NodeItem(
@Preview(showBackground = false) @Preview(showBackground = false)
fun NodeInfoSimplePreview() { fun NodeInfoSimplePreview() {
AppTheme { AppTheme {
val thisNode = NodeEntityPreviewParameterProvider().values.first() val thisNode = NodePreviewParameterProvider().values.first()
val thatNode = NodeEntityPreviewParameterProvider().values.last() val thatNode = NodePreviewParameterProvider().values.last()
NodeItem( NodeItem(
thisNode = thisNode, thisNode = thisNode,
thatNode = thatNode, thatNode = thatNode,
@ -312,11 +312,11 @@ fun NodeInfoSimplePreview() {
uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES,
) )
fun NodeInfoPreview( fun NodeInfoPreview(
@PreviewParameter(NodeEntityPreviewParameterProvider::class) @PreviewParameter(NodePreviewParameterProvider::class)
thatNode: NodeEntity thatNode: Node
) { ) {
AppTheme { AppTheme {
val thisNode = NodeEntityPreviewParameterProvider().values.first() val thisNode = NodePreviewParameterProvider().values.first()
Column { Column {
Text( Text(
text = "Details Collapsed", text = "Details Collapsed",

Wyświetl plik

@ -65,6 +65,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.model.RadioConfigState
import com.geeksville.mesh.model.RadioConfigViewModel import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.ui.components.PreferenceCategory import com.geeksville.mesh.ui.components.PreferenceCategory
import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog
@ -150,8 +151,7 @@ fun RadioConfigScreen(
} }
RadioConfigItemList( RadioConfigItemList(
enabled = state.connected && !isWaiting, state = state,
isLocal = state.isLocal,
modifier = modifier, modifier = modifier,
onRouteClick = { route -> onRouteClick = { route ->
isWaiting = true isWaiting = true
@ -285,28 +285,28 @@ private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Un
@Composable @Composable
private fun RadioConfigItemList( private fun RadioConfigItemList(
enabled: Boolean = true, state: RadioConfigState,
isLocal: Boolean = true,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onRouteClick: (Enum<*>) -> Unit = {}, onRouteClick: (Enum<*>) -> Unit = {},
onImport: () -> Unit = {}, onImport: () -> Unit = {},
onExport: () -> Unit = {}, onExport: () -> Unit = {},
) { ) {
val enabled = state.connected && !state.responseState.isWaiting()
LazyColumn( LazyColumn(
modifier = modifier, modifier = modifier,
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
) { ) {
item { PreferenceCategory(stringResource(R.string.device_settings)) } 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) } NavCard(title = it.title, icon = it.icon, enabled = enabled) { onRouteClick(it) }
} }
item { PreferenceCategory(stringResource(R.string.module_settings)) } 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) } NavCard(title = it.title, icon = it.icon, enabled = enabled) { onRouteClick(it) }
} }
if (isLocal) { if (state.isLocal) {
item { item {
PreferenceCategory("Backup & Restore") PreferenceCategory("Backup & Restore")
NavCard( NavCard(
@ -331,5 +331,7 @@ private fun RadioConfigItemList(
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun RadioSettingsScreenPreview() { private fun RadioSettingsScreenPreview() {
RadioConfigItemList() RadioConfigItemList(
RadioConfigState(isLocal = true, connected = true)
)
} }

Wyświetl plik

@ -39,7 +39,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.DataPacket import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.android.Logging 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.model.UIViewModel
import com.geeksville.mesh.ui.components.NodeMenuAction import com.geeksville.mesh.ui.components.NodeMenuAction
import com.geeksville.mesh.ui.components.NodeFilterTextField import com.geeksville.mesh.ui.components.NodeFilterTextField
@ -53,7 +53,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
private val model: UIViewModel by activityViewModels() 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 hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC // TODO use meta.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
val contactKey = "$channel${user.id}" val contactKey = "$channel${user.id}"
@ -91,7 +91,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
@Suppress("LongMethod") @Suppress("LongMethod")
fun NodesScreen( fun NodesScreen(
model: UIViewModel = hiltViewModel(), model: UIViewModel = hiltViewModel(),
navigateToMessages: (NodeEntity) -> Unit, navigateToMessages: (Node) -> Unit,
navigateToNodeDetails: (Int) -> Unit, navigateToNodeDetails: (Int) -> Unit,
) { ) {
val state by model.nodesUiState.collectAsStateWithLifecycle() val state by model.nodesUiState.collectAsStateWithLifecycle()

Wyświetl plik

@ -36,12 +36,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.model.Node
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
fun NodeMenu( fun NodeMenu(
node: NodeEntity, node: Node,
showFullMenu: Boolean = false, showFullMenu: Boolean = false,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
expanded: Boolean = false, expanded: Boolean = false,
@ -150,11 +150,11 @@ fun NodeMenu(
} }
sealed class NodeMenuAction { sealed class NodeMenuAction {
data class Remove(val node: NodeEntity) : NodeMenuAction() data class Remove(val node: Node) : NodeMenuAction()
data class Ignore(val node: NodeEntity) : NodeMenuAction() data class Ignore(val node: Node) : NodeMenuAction()
data class DirectMessage(val node: NodeEntity) : NodeMenuAction() data class DirectMessage(val node: Node) : NodeMenuAction()
data class RequestUserInfo(val node: NodeEntity) : NodeMenuAction() data class RequestUserInfo(val node: Node) : NodeMenuAction()
data class RequestPosition(val node: NodeEntity) : NodeMenuAction() data class RequestPosition(val node: Node) : NodeMenuAction()
data class TraceRoute(val node: NodeEntity) : NodeMenuAction() data class TraceRoute(val node: Node) : NodeMenuAction()
data class MoreDetails(val node: NodeEntity) : NodeMenuAction() data class MoreDetails(val node: Node) : NodeMenuAction()
} }

Wyświetl plik

@ -26,8 +26,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.model.Node
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.ui.theme.AppTheme
const val MAX_VALID_SNR = 100F const val MAX_VALID_SNR = 100F
@ -36,7 +36,7 @@ const val MAX_VALID_RSSI = 0
@Composable @Composable
fun SignalInfo( fun SignalInfo(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
node: NodeEntity, node: Node,
isThisNode: Boolean isThisNode: Boolean
) { ) {
val text = if (isThisNode) { val text = if (isThisNode) {
@ -81,7 +81,7 @@ fun SignalInfo(
fun SignalInfoSimplePreview() { fun SignalInfoSimplePreview() {
AppTheme { AppTheme {
SignalInfo( SignalInfo(
node = NodeEntity( node = Node(
num = 1, num = 1,
lastHeard = 0, lastHeard = 0,
channel = 0, channel = 0,
@ -97,8 +97,8 @@ fun SignalInfoSimplePreview() {
@PreviewLightDark @PreviewLightDark
@Composable @Composable
fun SignalInfoPreview( fun SignalInfoPreview(
@PreviewParameter(NodeEntityPreviewParameterProvider::class) @PreviewParameter(NodePreviewParameterProvider::class)
node: NodeEntity node: Node
) { ) {
AppTheme { AppTheme {
SignalInfo( SignalInfo(
@ -111,8 +111,8 @@ fun SignalInfoPreview(
@Composable @Composable
@PreviewLightDark @PreviewLightDark
fun SignalInfoSelfPreview( fun SignalInfoSelfPreview(
@PreviewParameter(NodeEntityPreviewParameterProvider::class) @PreviewParameter(NodePreviewParameterProvider::class)
node: NodeEntity node: Node
) { ) {
AppTheme { AppTheme {
SignalInfo( SignalInfo(

Wyświetl plik

@ -81,6 +81,8 @@ fun NetworkConfigScreen(
} }
NetworkConfigItemList( NetworkConfigItemList(
hasWifi = state.metadata?.hasWifi ?: true,
hasEthernet = state.metadata?.hasEthernet ?: true,
networkConfig = state.radioConfig.network, networkConfig = state.radioConfig.network,
enabled = state.connected, enabled = state.connected,
onSaveClicked = { networkInput -> onSaveClicked = { networkInput ->
@ -94,8 +96,11 @@ private fun extractWifiCredentials(qrCode: String) = Regex("""WIFI:S:(.*?);.*?P:
.find(qrCode)?.destructured .find(qrCode)?.destructured
?.let { (ssid, password) -> ssid to password } ?: (null to null) ?.let { (ssid, password) -> ssid to password } ?: (null to null)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable @Composable
fun NetworkConfigItemList( fun NetworkConfigItemList(
hasWifi: Boolean,
hasEthernet: Boolean,
networkConfig: NetworkConfig, networkConfig: NetworkConfig,
enabled: Boolean, enabled: Boolean,
onSaveClicked: (NetworkConfig) -> Unit, onSaveClicked: (NetworkConfig) -> Unit,
@ -137,16 +142,16 @@ fun NetworkConfigItemList(
item { item {
SwitchPreference(title = "WiFi enabled", SwitchPreference(title = "WiFi enabled",
checked = networkInput.wifiEnabled, checked = networkInput.wifiEnabled,
enabled = enabled, enabled = enabled && hasWifi,
onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } }) onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } })
Divider()
} }
item { Divider() }
item { item {
EditTextPreference(title = "SSID", EditTextPreference(title = "SSID",
value = networkInput.wifiSsid, value = networkInput.wifiSsid,
maxSize = 32, // wifi_ssid max_size:33 maxSize = 32, // wifi_ssid max_size:33
enabled = enabled, enabled = enabled && hasWifi,
isError = false, isError = false,
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
@ -161,7 +166,7 @@ fun NetworkConfigItemList(
EditPasswordPreference(title = "PSK", EditPasswordPreference(title = "PSK",
value = networkInput.wifiPsk, value = networkInput.wifiPsk,
maxSize = 64, // wifi_psk max_size:65 maxSize = 64, // wifi_psk max_size:65
enabled = enabled, enabled = enabled && hasWifi,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } }) onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } })
} }
@ -173,12 +178,20 @@ fun NetworkConfigItemList(
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.height(48.dp), .height(48.dp),
enabled = enabled, enabled = enabled && hasWifi,
) { ) {
Text(text = stringResource(R.string.wifi_qr_code_scan)) 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 { item {
EditTextPreference(title = "NTP server", EditTextPreference(title = "NTP server",
value = networkInput.ntpServer, 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 { item {
DropDownPreference(title = "IPv4 mode", DropDownPreference(title = "IPv4 mode",
enabled = enabled, enabled = enabled,
@ -225,8 +230,8 @@ fun NetworkConfigItemList(
.map { it to it.name }, .map { it to it.name },
selectedItem = networkInput.addressMode, selectedItem = networkInput.addressMode,
onItemSelected = { networkInput = networkInput.copy { addressMode = it } }) onItemSelected = { networkInput = networkInput.copy { addressMode = it } })
Divider()
} }
item { Divider() }
item { item {
EditIPv4Preference(title = "IP", EditIPv4Preference(title = "IP",
@ -292,6 +297,8 @@ fun NetworkConfigItemList(
@Composable @Composable
private fun NetworkConfigPreview() { private fun NetworkConfigPreview() {
NetworkConfigItemList( NetworkConfigItemList(
hasWifi = true,
hasEthernet = true,
networkConfig = NetworkConfig.getDefaultInstance(), networkConfig = NetworkConfig.getDefaultInstance(),
enabled = true, enabled = true,
onSaveClicked = { }, onSaveClicked = { },

Wyświetl plik

@ -65,8 +65,8 @@ import com.geeksville.mesh.android.gpsDisabled
import com.geeksville.mesh.android.hasGps import com.geeksville.mesh.android.hasGps
import com.geeksville.mesh.android.hasLocationPermission import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.copy import com.geeksville.mesh.copy
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.map.CustomTileSource import com.geeksville.mesh.model.map.CustomTileSource
import com.geeksville.mesh.model.map.MarkerWithLabel import com.geeksville.mesh.model.map.MarkerWithLabel
@ -311,7 +311,7 @@ fun MapView(
AppCompatResources.getDrawable(context, R.drawable.ic_baseline_location_on_24) 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 nodesWithPosition = nodes.filter { it.validPosition != null }
val ourNode = model.ourNodeInfo.value val ourNode = model.ourNodeInfo.value
val gpsFormat = model.config.display.gpsFormat.number val gpsFormat = model.config.display.gpsFormat.number

Wyświetl plik

@ -90,8 +90,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.DataPacket import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.QuickChatAction import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.getChannel import com.geeksville.mesh.model.getChannel
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
@ -116,7 +116,7 @@ internal fun FragmentManager.navigateToMessages(contactKey: String, message: Str
class MessagesFragment : Fragment(), Logging { class MessagesFragment : Fragment(), Logging {
private val model: UIViewModel by activityViewModels() 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 hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC // TODO use meta.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
val contactKey = "$channel${user.id}" val contactKey = "$channel${user.id}"
@ -168,7 +168,7 @@ internal fun MessageScreen(
contactKey: String, contactKey: String,
message: String, message: String,
viewModel: UIViewModel = hiltViewModel(), viewModel: UIViewModel = hiltViewModel(),
navigateToMessages: (NodeEntity) -> Unit, navigateToMessages: (Node) -> Unit,
navigateToNodeDetails: (Int) -> Unit, navigateToNodeDetails: (Int) -> Unit,
onNavigateBack: () -> Unit onNavigateBack: () -> Unit
) { ) {

Wyświetl plik

@ -60,16 +60,16 @@ import androidx.compose.ui.unit.sp
import com.geeksville.mesh.DataPacket import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MessageStatus import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R 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.components.AutoLinkText
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.ui.theme.AppTheme
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@Composable @Composable
internal fun MessageItem( internal fun MessageItem(
node: NodeEntity, node: Node,
messageText: String?, messageText: String?,
messageTime: String, messageTime: String,
messageStatus: MessageStatus?, messageStatus: MessageStatus?,
@ -197,7 +197,7 @@ internal fun MessageItem(
private fun MessageItemPreview() { private fun MessageItemPreview() {
AppTheme { AppTheme {
MessageItem( MessageItem(
node = NodeEntityPreviewParameterProvider().values.first(), node = NodePreviewParameterProvider().values.first(),
messageText = stringResource(R.string.sample_message), messageText = stringResource(R.string.sample_message),
messageTime = "10:00", messageTime = "10:00",
messageStatus = MessageStatus.DELIVERED, messageStatus = MessageStatus.DELIVERED,

Wyświetl plik

@ -20,19 +20,17 @@ package com.geeksville.mesh.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.DeviceMetrics.Companion.currentTime import com.geeksville.mesh.DeviceMetrics.Companion.currentTime
import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.deviceMetrics import com.geeksville.mesh.deviceMetrics
import com.geeksville.mesh.environmentMetrics import com.geeksville.mesh.environmentMetrics
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.paxcount import com.geeksville.mesh.paxcount
import com.geeksville.mesh.position import com.geeksville.mesh.position
import com.geeksville.mesh.telemetry
import com.geeksville.mesh.user import com.geeksville.mesh.user
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import kotlin.random.Random import kotlin.random.Random
class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity> { class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
val mickeyMouse = Node(
val mickeyMouse = NodeEntity(
num = 1955, num = 1955,
user = user { user = user {
id = "mickeyMouseId" id = "mickeyMouseId"
@ -40,28 +38,22 @@ class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity>
shortName = "MM" shortName = "MM"
hwModel = MeshProtos.HardwareModel.TBEAM hwModel = MeshProtos.HardwareModel.TBEAM
}, },
longName = "Mickey Mouse",
shortName = "MM",
position = position { position = position {
latitudeI = 338125110 latitudeI = 338125110
longitudeI = -1179189760 longitudeI = -1179189760
altitude = 138 altitude = 138
satsInView = 4 satsInView = 4
}, },
latitude = 33.812511,
longitude = -117.918976,
lastHeard = currentTime(), lastHeard = currentTime(),
channel = 0, channel = 0,
snr = 12.5F, snr = 12.5F,
rssi = -42, rssi = -42,
deviceTelemetry = telemetry { deviceMetrics = deviceMetrics {
deviceMetrics = deviceMetrics { channelUtilization = 2.4F
channelUtilization = 2.4F airUtilTx = 3.5F
airUtilTx = 3.5F batteryLevel = 85
batteryLevel = 85 voltage = 3.7F
voltage = 3.7F uptimeSeconds = 3600
uptimeSeconds = 3600
}
}, },
hopsAway = 0 hopsAway = 0
) )
@ -74,17 +66,13 @@ class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity>
id = "minnieMouseId" id = "minnieMouseId"
hwModel = MeshProtos.HardwareModel.HELTEC_V3 hwModel = MeshProtos.HardwareModel.HELTEC_V3
}, },
longName = "Minnie Mouse",
shortName = "MiMo",
snr = 12.5F, snr = 12.5F,
rssi = -42, rssi = -42,
position = position {}, position = position {},
latitude = 0.0,
longitude = 0.0,
hopsAway = 1 hopsAway = 1
) )
private val donaldDuck = NodeEntity( private val donaldDuck = Node(
num = Random.nextInt(), num = Random.nextInt(),
position = position { position = position {
latitudeI = 338052347 latitudeI = 338052347
@ -92,20 +80,16 @@ class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity>
altitude = 121 altitude = 121
satsInView = 66 satsInView = 66
}, },
latitude = 33.8052347,
longitude = -117.9208460,
lastHeard = currentTime() - 300, lastHeard = currentTime() - 300,
channel = 0, channel = 0,
snr = 12.5F, snr = 12.5F,
rssi = -42, rssi = -42,
deviceTelemetry = telemetry { deviceMetrics = deviceMetrics {
deviceMetrics = deviceMetrics { channelUtilization = 2.4F
channelUtilization = 2.4F airUtilTx = 3.5F
airUtilTx = 3.5F batteryLevel = 85
batteryLevel = 85 voltage = 3.7F
voltage = 3.7F uptimeSeconds = 3600
uptimeSeconds = 3600
}
}, },
user = user { user = user {
id = "donaldDuckId" id = "donaldDuckId"
@ -114,18 +98,14 @@ class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity>
hwModel = MeshProtos.HardwareModel.HELTEC_V3 hwModel = MeshProtos.HardwareModel.HELTEC_V3
publicKey = ByteString.copyFrom(ByteArray(32) { 1 }) publicKey = ByteString.copyFrom(ByteArray(32) { 1 })
}, },
longName = "Donald Duck, the Grand Duck of the Ducks", environmentMetrics = environmentMetrics {
shortName = "DoDu", temperature = 28.0F
environmentTelemetry = telemetry { relativeHumidity = 50.0F
environmentMetrics = environmentMetrics { barometricPressure = 1013.25F
temperature = 28.0F gasResistance = 0.0F
relativeHumidity = 50.0F voltage = 3.7F
barometricPressure = 1013.25F current = 0.0F
gasResistance = 0.0F iaq = 100
voltage = 3.7F
current = 0.0F
iaq = 100
}
}, },
paxcounter = paxcount { paxcounter = paxcount {
wifi = 30 wifi = 30
@ -142,19 +122,15 @@ class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity>
shortName = "myId" shortName = "myId"
hwModel = MeshProtos.HardwareModel.UNSET hwModel = MeshProtos.HardwareModel.UNSET
}, },
longName = "Meshtastic myId", environmentMetrics = environmentMetrics {},
shortName = null,
environmentTelemetry = telemetry {
environmentMetrics = environmentMetrics {}
},
paxcounter = paxcount {}, paxcounter = paxcount {},
) )
private val almostNothing = NodeEntity( private val almostNothing = Node(
num = Random.nextInt(), num = Random.nextInt(),
) )
override val values: Sequence<NodeEntity> override val values: Sequence<Node>
get() = sequenceOf( get() = sequenceOf(
mickeyMouse, // "this" node mickeyMouse, // "this" node
unknown, unknown,