kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat(config): implement excluded modules validation (#1460)
* feat(config): implement excluded modules validation * feat: hide excluded configs from metadata * refactor: save local metadata from WantConfig * refactor: delete metadata from deleted nodes * fix: always request metadata for admin routes * feat: show node firmware when metadata is available * refactor: rename filter function * feat: add `ServiceAction` request metadatapull/1517/head
rodzic
bdefbc3ce2
commit
60e7e18116
|
@ -0,0 +1,564 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 16,
|
||||||
|
"identityHash": "626fc53854f129654c1007b86d9fdda0",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "my_node",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, PRIMARY KEY(`myNodeNum`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "myNodeNum",
|
||||||
|
"columnName": "myNodeNum",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "model",
|
||||||
|
"columnName": "model",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "firmwareVersion",
|
||||||
|
"columnName": "firmwareVersion",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "couldUpdate",
|
||||||
|
"columnName": "couldUpdate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shouldUpdate",
|
||||||
|
"columnName": "shouldUpdate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "currentPacketId",
|
||||||
|
"columnName": "currentPacketId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "messageTimeoutMsec",
|
||||||
|
"columnName": "messageTimeoutMsec",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "minAppVersion",
|
||||||
|
"columnName": "minAppVersion",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxChannels",
|
||||||
|
"columnName": "maxChannels",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hasWifi",
|
||||||
|
"columnName": "hasWifi",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"myNodeNum"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "nodes",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `paxcounter` BLOB NOT NULL, PRIMARY KEY(`num`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "num",
|
||||||
|
"columnName": "num",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "user",
|
||||||
|
"columnName": "user",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "longName",
|
||||||
|
"columnName": "long_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shortName",
|
||||||
|
"columnName": "short_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "latitude",
|
||||||
|
"columnName": "latitude",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "longitude",
|
||||||
|
"columnName": "longitude",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "snr",
|
||||||
|
"columnName": "snr",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rssi",
|
||||||
|
"columnName": "rssi",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastHeard",
|
||||||
|
"columnName": "last_heard",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deviceTelemetry",
|
||||||
|
"columnName": "device_metrics",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "channel",
|
||||||
|
"columnName": "channel",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viaMqtt",
|
||||||
|
"columnName": "via_mqtt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hopsAway",
|
||||||
|
"columnName": "hops_away",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isFavorite",
|
||||||
|
"columnName": "is_favorite",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isIgnored",
|
||||||
|
"columnName": "is_ignored",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "environmentTelemetry",
|
||||||
|
"columnName": "environment_metrics",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "powerTelemetry",
|
||||||
|
"columnName": "power_metrics",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "paxcounter",
|
||||||
|
"columnName": "paxcounter",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"num"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "packet",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `reply_id` INTEGER NOT NULL DEFAULT 0)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uuid",
|
||||||
|
"columnName": "uuid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "myNodeNum",
|
||||||
|
"columnName": "myNodeNum",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "port_num",
|
||||||
|
"columnName": "port_num",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contact_key",
|
||||||
|
"columnName": "contact_key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "received_time",
|
||||||
|
"columnName": "received_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "read",
|
||||||
|
"columnName": "read",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "data",
|
||||||
|
"columnName": "data",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "packetId",
|
||||||
|
"columnName": "packet_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "routingError",
|
||||||
|
"columnName": "routing_error",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "replyId",
|
||||||
|
"columnName": "reply_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_packet_myNodeNum",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"myNodeNum"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_packet_port_num",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"port_num"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_packet_contact_key",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"contact_key"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "contact_settings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, PRIMARY KEY(`contact_key`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "contact_key",
|
||||||
|
"columnName": "contact_key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muteUntil",
|
||||||
|
"columnName": "muteUntil",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"contact_key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "log",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uuid",
|
||||||
|
"columnName": "uuid",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "message_type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "received_date",
|
||||||
|
"columnName": "received_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "raw_message",
|
||||||
|
"columnName": "message",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fromNum",
|
||||||
|
"columnName": "from_num",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "portNum",
|
||||||
|
"columnName": "port_num",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fromRadio",
|
||||||
|
"columnName": "from_radio",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "x''"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_log_from_num",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"from_num"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_log_port_num",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"port_num"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "quick_chat",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uuid",
|
||||||
|
"columnName": "uuid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "message",
|
||||||
|
"columnName": "message",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mode",
|
||||||
|
"columnName": "mode",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "reactions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`reply_id`, `user_id`, `emoji`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "replyId",
|
||||||
|
"columnName": "reply_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emoji",
|
||||||
|
"columnName": "emoji",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"reply_id",
|
||||||
|
"user_id",
|
||||||
|
"emoji"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_reactions_reply_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"reply_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "metadata",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "num",
|
||||||
|
"columnName": "num",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "proto",
|
||||||
|
"columnName": "proto",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"num"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_metadata_num",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"num"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '626fc53854f129654c1007b86d9fdda0')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,8 +24,10 @@ import com.geeksville.mesh.database.MeshtasticDatabase
|
||||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
import com.geeksville.mesh.database.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Meshtastic LLC
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.geeksville.mesh.model
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
|
||||||
|
import com.geeksville.mesh.MeshProtos
|
||||||
|
import com.geeksville.mesh.PaxcountProtos
|
||||||
|
import com.geeksville.mesh.TelemetryProtos.DeviceMetrics
|
||||||
|
import com.geeksville.mesh.TelemetryProtos.EnvironmentMetrics
|
||||||
|
import com.geeksville.mesh.TelemetryProtos.PowerMetrics
|
||||||
|
import com.geeksville.mesh.util.GPSFormat
|
||||||
|
import com.geeksville.mesh.util.latLongToMeter
|
||||||
|
import com.geeksville.mesh.util.toDistanceString
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
data class Node(
|
||||||
|
val num: Int,
|
||||||
|
val metadata: MeshProtos.DeviceMetadata? = null,
|
||||||
|
val user: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
|
||||||
|
val position: MeshProtos.Position = MeshProtos.Position.getDefaultInstance(),
|
||||||
|
val snr: Float = Float.MAX_VALUE,
|
||||||
|
val rssi: Int = Int.MAX_VALUE,
|
||||||
|
val lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
|
||||||
|
val deviceMetrics: DeviceMetrics = DeviceMetrics.getDefaultInstance(),
|
||||||
|
val channel: Int = 0,
|
||||||
|
val viaMqtt: Boolean = false,
|
||||||
|
val hopsAway: Int = -1,
|
||||||
|
val isFavorite: Boolean = false,
|
||||||
|
val isIgnored: Boolean = false,
|
||||||
|
val environmentMetrics: EnvironmentMetrics = EnvironmentMetrics.getDefaultInstance(),
|
||||||
|
val powerMetrics: PowerMetrics = PowerMetrics.getDefaultInstance(),
|
||||||
|
val paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(),
|
||||||
|
) {
|
||||||
|
val colors: Pair<Int, Int>
|
||||||
|
get() { // returns foreground and background @ColorInt for each 'num'
|
||||||
|
val r = (num and 0xFF0000) shr 16
|
||||||
|
val g = (num and 0x00FF00) shr 8
|
||||||
|
val b = num and 0x0000FF
|
||||||
|
val brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255
|
||||||
|
return (if (brightness > 0.5) Color.BLACK else Color.WHITE) to Color.rgb(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isUnknownUser get() = user.hwModel == MeshProtos.HardwareModel.UNSET
|
||||||
|
val hasPKC get() = !user.publicKey.isEmpty
|
||||||
|
val errorByteString: ByteString get() = ByteString.copyFrom(ByteArray(32) { 0 })
|
||||||
|
val mismatchKey get() = user.publicKey == errorByteString
|
||||||
|
|
||||||
|
val hasEnvironmentMetrics: Boolean
|
||||||
|
get() = environmentMetrics != EnvironmentMetrics.getDefaultInstance()
|
||||||
|
|
||||||
|
val hasPowerMetrics: Boolean
|
||||||
|
get() = powerMetrics != PowerMetrics.getDefaultInstance()
|
||||||
|
|
||||||
|
val batteryLevel get() = deviceMetrics.batteryLevel
|
||||||
|
val voltage get() = deviceMetrics.voltage
|
||||||
|
val batteryStr get() = if (batteryLevel in 1..100) "$batteryLevel%" else ""
|
||||||
|
|
||||||
|
val latitude get() = position.latitudeI * 1e-7
|
||||||
|
val longitude get() = position.longitudeI * 1e-7
|
||||||
|
|
||||||
|
private fun hasValidPosition(): Boolean {
|
||||||
|
return latitude != 0.0 && longitude != 0.0 &&
|
||||||
|
(latitude >= -90 && latitude <= 90.0) &&
|
||||||
|
(longitude >= -180 && longitude <= 180)
|
||||||
|
}
|
||||||
|
|
||||||
|
val validPosition: MeshProtos.Position? get() = position.takeIf { hasValidPosition() }
|
||||||
|
|
||||||
|
// @return distance in meters to some other node (or null if unknown)
|
||||||
|
fun distance(o: Node): Int? = when {
|
||||||
|
validPosition == null || o.validPosition == null -> null
|
||||||
|
else -> latLongToMeter(latitude, longitude, o.latitude, o.longitude).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// @return a nice human readable string for the distance, or null for unknown
|
||||||
|
fun distanceStr(o: Node, displayUnits: Int = 0): String? = distance(o)?.let { dist ->
|
||||||
|
val system = DisplayConfig.DisplayUnits.forNumber(displayUnits)
|
||||||
|
return if (dist > 0) dist.toDistanceString(system) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
// @return bearing to the other position in degrees
|
||||||
|
fun bearing(o: Node?): Int? = when {
|
||||||
|
validPosition == null || o?.validPosition == null -> null
|
||||||
|
else -> com.geeksville.mesh.util.bearing(latitude, longitude, o.latitude, o.longitude).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun gpsString(gpsFormat: Int): String = when (gpsFormat) {
|
||||||
|
DisplayConfig.GpsCoordinateFormat.DEC_VALUE -> GPSFormat.toDEC(latitude, longitude)
|
||||||
|
DisplayConfig.GpsCoordinateFormat.DMS_VALUE -> GPSFormat.toDMS(latitude, longitude)
|
||||||
|
DisplayConfig.GpsCoordinateFormat.UTM_VALUE -> GPSFormat.toUTM(latitude, longitude)
|
||||||
|
DisplayConfig.GpsCoordinateFormat.MGRS_VALUE -> GPSFormat.toMGRS(latitude, longitude)
|
||||||
|
else -> GPSFormat.toDEC(latitude, longitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun EnvironmentMetrics.getDisplayString(isFahrenheit: Boolean): String {
|
||||||
|
val temp = if (temperature != 0f) {
|
||||||
|
if (isFahrenheit) {
|
||||||
|
val fahrenheit = temperature * 1.8F + 32
|
||||||
|
"%.1f°F".format(fahrenheit)
|
||||||
|
} else {
|
||||||
|
"%.1f°C".format(temperature)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val humidity = if (relativeHumidity != 0f) "%.0f%%".format(relativeHumidity) else null
|
||||||
|
val voltage = if (this.voltage != 0f) "%.2fV".format(this.voltage) else null
|
||||||
|
val current = if (current != 0f) "%.1fmA".format(current) else null
|
||||||
|
val iaq = if (iaq != 0) "IAQ: $iaq" else null
|
||||||
|
|
||||||
|
return listOfNotNull(
|
||||||
|
temp,
|
||||||
|
humidity,
|
||||||
|
voltage,
|
||||||
|
current,
|
||||||
|
iaq,
|
||||||
|
).joinToString(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PaxcountProtos.Paxcount.getDisplayString() =
|
||||||
|
"PAX: ${ble + wifi} (B:$ble/W:$wifi)".takeIf { ble != 0 && wifi != 0 }
|
||||||
|
|
||||||
|
fun getTelemetryString(isFahrenheit: Boolean = false): String {
|
||||||
|
return listOfNotNull(
|
||||||
|
paxcounter.getDisplayString(),
|
||||||
|
environmentMetrics.getDisplayString(isFahrenheit),
|
||||||
|
).joinToString(" ")
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,6 @@ import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.android.Logging
|
import com.geeksville.mesh.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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 = { },
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
Ładowanie…
Reference in New Issue