From 80d9a2e0aac5007ee9950367bdad59c895106cf1 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:23:19 -0600 Subject: [PATCH] fix(release): fixes to prep for release (#4546) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../com/geeksville/mesh/model/BTScanModel.kt | 2 +- .../mesh/service/FromRadioPacketHandler.kt | 2 +- .../mesh/service/MeshActionHandler.kt | 2 + .../mesh/service/MeshConfigHandler.kt | 12 +- .../mesh/service/MeshDataHandler.kt | 8 + .../geeksville/mesh/service/MeshDataMapper.kt | 1 + .../mesh/service/MeshMessageProcessor.kt | 14 +- .../mesh/service/MeshNodeManager.kt | 6 +- .../service/FromRadioPacketHandlerTest.kt | 2 +- .../36.json | 1009 ++++++++++++++++ .../37.json | 1016 +++++++++++++++++ .../core/database/MeshtasticDatabase.kt | 4 +- .../core/database/entity/NodeEntity.kt | 7 + .../meshtastic/core/database/entity/Packet.kt | 1 + .../meshtastic/core/database/model/Message.kt | 3 + .../meshtastic/core/database/model/Node.kt | 30 + .../org/meshtastic/core/model/DataPacket.kt | 4 + .../org/meshtastic/core/model/NodeInfo.kt | 1 + .../meshtastic/core/model/util/Extensions.kt | 6 + .../core/service/ServiceRepository.kt | 10 +- .../composeResources/values/strings.xml | 3 + .../core/ui/component/TransportIcon.kt | 50 + .../org/meshtastic/core/ui/icon/Status.kt | 10 + .../messaging/component/MessageItem.kt | 24 +- .../node/component/DeviceDetailsSection.kt | 1 + .../node/component/NodeDetailsSection.kt | 32 +- .../feature/node/component/NodeItem.kt | 61 +- .../feature/node/detail/NodeDetailList.kt | 274 ----- .../feature/node/detail/NodeDetailScreen.kt | 32 +- .../node/detail/NodeDetailViewModel.kt | 9 +- 30 files changed, 2324 insertions(+), 312 deletions(-) create mode 100644 core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/36.json create mode 100644 core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/37.json create mode 100644 core/ui/src/main/kotlin/org/meshtastic/core/ui/component/TransportIcon.kt delete mode 100644 feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailList.kt diff --git a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt index 319e07008..708de3bc9 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt @@ -162,7 +162,7 @@ constructor( val spinner: StateFlow = bluetoothRepository.isScanning init { - serviceRepository.statusMessage.onEach { errorText.value = it }.launchIn(viewModelScope) + serviceRepository.connectionProgress.onEach { errorText.value = it }.launchIn(viewModelScope) Logger.d { "BTScanModel created" } } diff --git a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt b/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt index 7b4ba2c07..a771b6fa2 100644 --- a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt @@ -55,7 +55,7 @@ constructor( metadata != null -> router.configFlowManager.handleLocalMetadata(metadata) nodeInfo != null -> { router.configFlowManager.handleNodeInfo(nodeInfo) - serviceRepository.setStatusMessage("Nodes (${router.configFlowManager.newNodeCount})") + serviceRepository.setConnectionProgress("Nodes (${router.configFlowManager.newNodeCount})") } configCompleteId != null -> router.configFlowManager.handleConfigComplete(configCompleteId) mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt index 973366159..b4b4dbe1e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt @@ -208,6 +208,7 @@ constructor( fun handleSetRemoteOwner(id: Int, destNum: Int, payload: ByteArray) { val u = User.ADAPTER.decode(payload) commandSender.sendAdmin(destNum, id) { AdminMessage(set_owner = u) } + nodeManager.handleReceivedUser(destNum, u) } fun handleGetRemoteOwner(id: Int, destNum: Int) { @@ -237,6 +238,7 @@ constructor( fun handleSetModuleConfig(id: Int, destNum: Int, payload: ByteArray) { val c = ModuleConfig.ADAPTER.decode(payload) commandSender.sendAdmin(destNum, id) { AdminMessage(set_module_config = c) } + c.statusmessage?.node_status?.let { status -> nodeManager.updateNodeStatus(destNum, status) } } fun handleGetModuleConfig(id: Int, destNum: Int, config: Int) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt index 3332a221d..4135185b1 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt @@ -59,12 +59,16 @@ constructor( fun handleDeviceConfig(config: Config) { scope.handledLaunch { radioConfigRepository.setLocalConfig(config) } - serviceRepository.setStatusMessage("Device config received") + serviceRepository.setConnectionProgress("Device config received") } fun handleModuleConfig(config: ModuleConfig) { scope.handledLaunch { radioConfigRepository.setLocalModuleConfig(config) } - serviceRepository.setStatusMessage("Module config received") + serviceRepository.setConnectionProgress("Module config received") + + config.statusmessage?.node_status?.let { status -> + nodeManager.myNodeNum?.let { num -> nodeManager.updateNodeStatus(num, status) } + } } fun handleChannel(ch: Channel) { @@ -75,9 +79,9 @@ constructor( val mi = nodeManager.getMyNodeInfo() val index = ch.index ?: 0 if (mi != null) { - serviceRepository.setStatusMessage("Channels (${index + 1} / ${mi.maxChannels})") + serviceRepository.setConnectionProgress("Channels (${index + 1} / ${mi.maxChannels})") } else { - serviceRepository.setStatusMessage("Channels (${index + 1})") + serviceRepository.setConnectionProgress("Channels (${index + 1})") } } } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt index 79eef1e29..c62049219 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt @@ -335,6 +335,14 @@ constructor( u.session_passkey.let { commandSender.setSessionPasskey(it) } val fromNum = packet.from + u.get_module_config_response?.let { config -> + if (fromNum == myNodeNum) { + configHandler.handleModuleConfig(config) + } else { + config.statusmessage?.node_status?.let { nodeManager.updateNodeStatus(fromNum, it) } + } + } + if (fromNum == myNodeNum) { u.get_config_response?.let { configHandler.handleDeviceConfig(it) } u.get_channel_response?.let { configHandler.handleChannel(it) } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt index 74a24eeac..bf9450b30 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataMapper.kt @@ -49,6 +49,7 @@ class MeshDataMapper @Inject constructor(private val nodeManager: MeshNodeManage relayNode = packet.relay_node, viaMqtt = packet.via_mqtt == true, emoji = decoded.emoji, + transportMechanism = packet.transport_mechanism.value, ) } } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt b/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt index 348089f46..2096e203f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog +import org.meshtastic.core.model.util.isLora import org.meshtastic.core.service.ServiceRepository import org.meshtastic.proto.FromRadio import org.meshtastic.proto.LogRecord @@ -205,11 +206,20 @@ constructor( } nodeManager.updateNodeInfo(from, withBroadcast = false, channel = packet.channel) { it.lastHeard = packet.rx_time - it.snr = packet.rx_snr - it.rssi = packet.rx_rssi + it.viaMqtt = packet.via_mqtt == true + it.lastTransport = packet.transport_mechanism.value + + val isDirect = packet.hop_start == packet.hop_limit + if (isDirect && packet.isLora() && !it.viaMqtt) { + it.snr = packet.rx_snr + it.rssi = packet.rx_rssi + } + it.hopsAway = if (decoded.portnum == PortNum.RANGE_TEST_APP) { 0 + } else if (it.viaMqtt) { + -1 } else if (packet.hop_start == 0 && (decoded.bitfield ?: 0) == 0) { -1 } else if (packet.hop_limit > packet.hop_start) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.kt index 5a39d1a39..645f73914 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.kt @@ -214,7 +214,11 @@ constructor( } fun handleReceivedNodeStatus(fromNum: Int, s: StatusMessage) { - updateNodeInfo(fromNum) { it.nodeStatus = s.status } + updateNodeStatus(fromNum, s.status) + } + + fun updateNodeStatus(nodeNum: Int, status: String) { + updateNodeInfo(nodeNum) { it.nodeStatus = status } } fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean = true) { diff --git a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt b/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt index 5b4ffa1d8..82b26c6e6 100644 --- a/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/FromRadioPacketHandlerTest.kt @@ -72,7 +72,7 @@ class FromRadioPacketHandlerTest { handler.handleFromRadio(proto) verify { router.configFlowManager.handleNodeInfo(nodeInfo) } - verify { serviceRepository.setStatusMessage(any()) } + verify { serviceRepository.setConnectionProgress(any()) } } @Test diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/36.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/36.json new file mode 100644 index 000000000..86a111b18 --- /dev/null +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/36.json @@ -0,0 +1,1009 @@ +{ + "formatVersion": 1, + "database": { + "version": 36, + "identityHash": "25bf8e7feb6d0e7f9eab4dfccf546e45", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "pioEnv", + "columnName": "pioEnv", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMuted", + "columnName": "is_muted", + "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 + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "manuallyVerified", + "columnName": "manually_verified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nodeStatus", + "columnName": "node_status", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_nodes_last_heard", + "unique": false, + "columnNames": [ + "last_heard" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)" + }, + { + "name": "index_nodes_short_name", + "unique": false, + "columnNames": [ + "short_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)" + }, + { + "name": "index_nodes_long_name", + "unique": false, + "columnNames": [ + "long_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)" + }, + { + "name": "index_nodes_hops_away", + "unique": false, + "columnNames": [ + "hops_away" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)" + }, + { + "name": "index_nodes_is_favorite", + "unique": false, + "columnNames": [ + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)" + }, + { + "name": "index_nodes_last_heard_is_favorite", + "unique": false, + "columnNames": [ + "last_heard", + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)" + } + ] + }, + { + "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, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` 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": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "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`)" + }, + { + "name": "index_packet_contact_key_port_num_received_time", + "unique": false, + "columnNames": [ + "contact_key", + "port_num", + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)" + }, + { + "name": "index_packet_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessageUuid", + "columnName": "last_read_message_uuid", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReadMessageTimestamp", + "columnName": "last_read_message_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "filteringDisabled", + "columnName": "filtering_disabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "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 + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relayNode", + "columnName": "relay_node", + "affinity": "INTEGER" + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT" + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum", + "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`)" + }, + { + "name": "index_reactions_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "platformio_target" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "traceroute_node_position", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "logUuid", + "columnName": "log_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestId", + "columnName": "request_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "log_uuid", + "node_num" + ] + }, + "indices": [ + { + "name": "index_traceroute_node_position_log_uuid", + "unique": false, + "columnNames": [ + "log_uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)" + }, + { + "name": "index_traceroute_node_position_request_id", + "unique": false, + "columnNames": [ + "request_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)" + } + ], + "foreignKeys": [ + { + "table": "log", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "log_uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + } + ], + "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, '25bf8e7feb6d0e7f9eab4dfccf546e45')" + ] + } +} \ No newline at end of file diff --git a/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/37.json b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/37.json new file mode 100644 index 000000000..983dbfa8e --- /dev/null +++ b/core/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/37.json @@ -0,0 +1,1016 @@ +{ + "formatVersion": 1, + "database": { + "version": 37, + "identityHash": "3aa6a9878f55d28c30fea04e0c572f89", + "entities": [ + { + "tableName": "my_node", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, `pioEnv` TEXT, PRIMARY KEY(`myNodeNum`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT" + }, + { + "fieldPath": "firmwareVersion", + "columnName": "firmwareVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "couldUpdate", + "columnName": "couldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shouldUpdate", + "columnName": "shouldUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentPacketId", + "columnName": "currentPacketId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageTimeoutMsec", + "columnName": "messageTimeoutMsec", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAppVersion", + "columnName": "minAppVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxChannels", + "columnName": "maxChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasWifi", + "columnName": "hasWifi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "pioEnv", + "columnName": "pioEnv", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum" + ] + } + }, + { + "tableName": "nodes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `is_muted` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `paxcounter` BLOB NOT NULL, `public_key` BLOB, `notes` TEXT NOT NULL DEFAULT '', `manually_verified` INTEGER NOT NULL DEFAULT 0, `node_status` TEXT, `last_transport` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "long_name", + "affinity": "TEXT" + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT" + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastHeard", + "columnName": "last_heard", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceTelemetry", + "columnName": "device_metrics", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viaMqtt", + "columnName": "via_mqtt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hopsAway", + "columnName": "hops_away", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "is_favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isIgnored", + "columnName": "is_ignored", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isMuted", + "columnName": "is_muted", + "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 + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "manuallyVerified", + "columnName": "manually_verified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nodeStatus", + "columnName": "node_status", + "affinity": "TEXT" + }, + { + "fieldPath": "lastTransport", + "columnName": "last_transport", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_nodes_last_heard", + "unique": false, + "columnNames": [ + "last_heard" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard` ON `${TABLE_NAME}` (`last_heard`)" + }, + { + "name": "index_nodes_short_name", + "unique": false, + "columnNames": [ + "short_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_short_name` ON `${TABLE_NAME}` (`short_name`)" + }, + { + "name": "index_nodes_long_name", + "unique": false, + "columnNames": [ + "long_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_long_name` ON `${TABLE_NAME}` (`long_name`)" + }, + { + "name": "index_nodes_hops_away", + "unique": false, + "columnNames": [ + "hops_away" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_hops_away` ON `${TABLE_NAME}` (`hops_away`)" + }, + { + "name": "index_nodes_is_favorite", + "unique": false, + "columnNames": [ + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_is_favorite` ON `${TABLE_NAME}` (`is_favorite`)" + }, + { + "name": "index_nodes_last_heard_is_favorite", + "unique": false, + "columnNames": [ + "last_heard", + "is_favorite" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_nodes_last_heard_is_favorite` ON `${TABLE_NAME}` (`last_heard`, `is_favorite`)" + } + ] + }, + { + "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, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `sfpp_hash` BLOB, `filtered` 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": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "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`)" + }, + { + "name": "index_packet_contact_key_port_num_received_time", + "unique": false, + "columnNames": [ + "contact_key", + "port_num", + "received_time" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key_port_num_received_time` ON `${TABLE_NAME}` (`contact_key`, `port_num`, `received_time`)" + }, + { + "name": "index_packet_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "contact_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, `last_read_message_uuid` INTEGER, `last_read_message_timestamp` INTEGER, `filtering_disabled` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`contact_key`))", + "fields": [ + { + "fieldPath": "contact_key", + "columnName": "contact_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "muteUntil", + "columnName": "muteUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessageUuid", + "columnName": "last_read_message_uuid", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastReadMessageTimestamp", + "columnName": "last_read_message_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "filteringDisabled", + "columnName": "filtering_disabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "contact_key" + ] + } + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message_type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "received_date", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw_message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromNum", + "columnName": "from_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "portNum", + "columnName": "port_num", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fromRadio", + "columnName": "from_radio", + "affinity": "BLOB", + "notNull": true, + "defaultValue": "x''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [ + { + "name": "index_log_from_num", + "unique": false, + "columnNames": [ + "from_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)" + }, + { + "name": "index_log_port_num", + "unique": false, + "columnNames": [ + "port_num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)" + } + ] + }, + { + "tableName": "quick_chat", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "reactions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL DEFAULT 0, `reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `snr` REAL NOT NULL DEFAULT 0, `rssi` INTEGER NOT NULL DEFAULT 0, `hopsAway` INTEGER NOT NULL DEFAULT -1, `packet_id` INTEGER NOT NULL DEFAULT 0, `status` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT 0, `relays` INTEGER NOT NULL DEFAULT 0, `relay_node` INTEGER, `to` TEXT, `channel` INTEGER NOT NULL DEFAULT 0, `sfpp_hash` BLOB, PRIMARY KEY(`myNodeNum`, `reply_id`, `user_id`, `emoji`))", + "fields": [ + { + "fieldPath": "myNodeNum", + "columnName": "myNodeNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "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 + }, + { + "fieldPath": "snr", + "columnName": "snr", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hopsAway", + "columnName": "hopsAway", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "packetId", + "columnName": "packet_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "routingError", + "columnName": "routing_error", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "relayNode", + "columnName": "relay_node", + "affinity": "INTEGER" + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT" + }, + { + "fieldPath": "channel", + "columnName": "channel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sfpp_hash", + "columnName": "sfpp_hash", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "myNodeNum", + "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`)" + }, + { + "name": "index_reactions_packet_id", + "unique": false, + "columnNames": [ + "packet_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_packet_id` ON `${TABLE_NAME}` (`packet_id`)" + } + ] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))", + "fields": [ + { + "fieldPath": "num", + "columnName": "num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "proto", + "columnName": "proto", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "num" + ] + }, + "indices": [ + { + "name": "index_metadata_num", + "unique": false, + "columnNames": [ + "num" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)" + } + ] + }, + { + "tableName": "device_hardware", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`platformio_target`))", + "fields": [ + { + "fieldPath": "activelySupported", + "columnName": "actively_supported", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "architecture", + "columnName": "architecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasInkHud", + "columnName": "has_ink_hud", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasMui", + "columnName": "has_mui", + "affinity": "INTEGER" + }, + { + "fieldPath": "hwModel", + "columnName": "hwModel", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hwModelSlug", + "columnName": "hw_model_slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partitionScheme", + "columnName": "partition_scheme", + "affinity": "TEXT" + }, + { + "fieldPath": "platformioTarget", + "columnName": "platformio_target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiresDfu", + "columnName": "requires_dfu", + "affinity": "INTEGER" + }, + { + "fieldPath": "supportLevel", + "columnName": "support_level", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "platformio_target" + ] + } + }, + { + "tableName": "firmware_release", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pageUrl", + "columnName": "page_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "release_notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "zipUrl", + "columnName": "zip_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseType", + "columnName": "release_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "traceroute_node_position", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`log_uuid` TEXT NOT NULL, `request_id` INTEGER NOT NULL, `node_num` INTEGER NOT NULL, `position` BLOB NOT NULL, PRIMARY KEY(`log_uuid`, `node_num`), FOREIGN KEY(`log_uuid`) REFERENCES `log`(`uuid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "logUuid", + "columnName": "log_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestId", + "columnName": "request_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeNum", + "columnName": "node_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "log_uuid", + "node_num" + ] + }, + "indices": [ + { + "name": "index_traceroute_node_position_log_uuid", + "unique": false, + "columnNames": [ + "log_uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_log_uuid` ON `${TABLE_NAME}` (`log_uuid`)" + }, + { + "name": "index_traceroute_node_position_request_id", + "unique": false, + "columnNames": [ + "request_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_traceroute_node_position_request_id` ON `${TABLE_NAME}` (`request_id`)" + } + ], + "foreignKeys": [ + { + "table": "log", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "log_uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + } + ], + "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, '3aa6a9878f55d28c30fea04e0c572f89')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt index babdbe3c7..de950c15a 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt @@ -93,8 +93,10 @@ import org.meshtastic.core.database.entity.TracerouteNodePositionEntity AutoMigration(from = 32, to = 33), AutoMigration(from = 33, to = 34, spec = AutoMigration33to34::class), AutoMigration(from = 34, to = 35, spec = AutoMigration34to35::class), + AutoMigration(from = 35, to = 36), + AutoMigration(from = 36, to = 37), ], - version = 35, + version = 37, exportSchema = true, ) @TypeConverters(Converters::class) diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt index 8160aa904..fc2482f68 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt @@ -33,6 +33,7 @@ import org.meshtastic.core.model.Position import org.meshtastic.core.model.util.onlineTimeThreshold import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.HardwareModel +import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.Paxcount import org.meshtastic.proto.Telemetry import org.meshtastic.proto.User @@ -65,6 +66,7 @@ data class NodeWithRelations( notes = notes, manuallyVerified = manuallyVerified, nodeStatus = nodeStatus, + lastTransport = lastTransport, ) } @@ -89,6 +91,7 @@ data class NodeWithRelations( notes = notes, manuallyVerified = manuallyVerified, nodeStatus = nodeStatus, + lastTransport = lastTransport, ) } } @@ -140,6 +143,8 @@ data class NodeEntity( @ColumnInfo(name = "manually_verified", defaultValue = "0") var manuallyVerified: Boolean = false, // ONLY set true when scanned/imported manually @ColumnInfo(name = "node_status") var nodeStatus: String? = null, + /** The transport mechanism this node was last heard over (see [MeshPacket.TransportMechanism]). */ + @ColumnInfo(name = "last_transport", defaultValue = "0") var lastTransport: Int = 0, ) { val deviceMetrics: org.meshtastic.proto.DeviceMetrics? get() = deviceTelemetry.device_metrics @@ -199,6 +204,7 @@ data class NodeEntity( publicKey = publicKey ?: user.public_key, notes = notes, nodeStatus = nodeStatus, + lastTransport = lastTransport, ) fun toNodeInfo() = NodeInfo( @@ -243,5 +249,6 @@ data class NodeEntity( environmentTelemetry.time, ), hopsAway = hopsAway, + nodeStatus = nodeStatus, ) } diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt index 999b5d515..8d8b0dab7 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt @@ -58,6 +58,7 @@ data class PacketEntity( relayNode = data.relayNode, relays = data.relays, filtered = filtered, + transportMechanism = data.transportMechanism, ) } } diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt index e5c71cfb6..459e0f815 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt @@ -46,6 +46,7 @@ import org.meshtastic.core.strings.routing_error_rate_limit_exceeded import org.meshtastic.core.strings.routing_error_timeout import org.meshtastic.core.strings.routing_error_too_large import org.meshtastic.core.strings.unrecognized +import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.Routing @Suppress("CyclomaticComplexMethod") @@ -92,6 +93,8 @@ data class Message( val relayNode: Int? = null, val relays: Int = 0, val filtered: Boolean = false, + /** The transport mechanism this packet arrived over (see [MeshPacket.TransportMechanism]). */ + val transportMechanism: Int = 0, ) { fun getStatusStringRes(): Pair { val title = if (routingError > 0) Res.string.error else Res.string.message_delivery_status diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt index 3eed12abb..1207ead19 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt @@ -29,9 +29,11 @@ import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.DeviceMetrics import org.meshtastic.proto.EnvironmentMetrics import org.meshtastic.proto.HardwareModel +import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.Paxcount import org.meshtastic.proto.Position import org.meshtastic.proto.PowerMetrics +import org.meshtastic.proto.Telemetry import org.meshtastic.proto.User @Suppress("MagicNumber") @@ -57,6 +59,8 @@ data class Node( val notes: String = "", val manuallyVerified: Boolean = false, val nodeStatus: String? = null, + /** The transport mechanism this node was last heard over (see [MeshPacket.TransportMechanism]). */ + val lastTransport: Int = 0, ) { val capabilities: Capabilities by lazy { Capabilities(metadata?.firmware_version) } @@ -175,6 +179,32 @@ data class Node( fun getTelemetryStrings(isFahrenheit: Boolean = false): List = environmentMetrics.getDisplayStrings(isFahrenheit) + + fun toEntity() = NodeEntity( + num = num, + user = user, + position = position, + latitude = latitude, + longitude = longitude, + snr = snr, + rssi = rssi, + lastHeard = lastHeard, + deviceTelemetry = Telemetry(device_metrics = deviceMetrics), + channel = channel, + viaMqtt = viaMqtt, + hopsAway = hopsAway, + isFavorite = isFavorite, + isIgnored = isIgnored, + isMuted = isMuted, + environmentTelemetry = Telemetry(environment_metrics = environmentMetrics), + powerTelemetry = Telemetry(power_metrics = powerMetrics), + paxcounter = paxcounter, + publicKey = publicKey ?: user.public_key, + notes = notes, + manuallyVerified = manuallyVerified, + nodeStatus = nodeStatus, + lastTransport = lastTransport, + ) } fun Config.DeviceConfig.Role?.isUnmessageableRole(): Boolean = this in diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt index 033268755..b5cd4d505 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt @@ -27,6 +27,7 @@ import okio.ByteString import okio.ByteString.Companion.toByteString import org.meshtastic.core.model.util.ByteStringParceler import org.meshtastic.core.model.util.ByteStringSerializer +import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum import org.meshtastic.proto.Waypoint @@ -70,6 +71,8 @@ data class DataPacket( @Serializable(with = ByteStringSerializer::class) @TypeParceler var sfppHash: ByteString? = null, + /** The transport mechanism this packet arrived over (see [MeshPacket.TransportMechanism]). */ + var transportMechanism: Int = 0, ) : Parcelable { fun readFromParcel(parcel: Parcel) { @@ -108,6 +111,7 @@ data class DataPacket( viaMqtt = parcel.readInt() != 0 emoji = parcel.readInt() sfppHash = ByteStringParceler.create(parcel) + transportMechanism = parcel.readInt() } /** If there was an error with this message, this string describes what was wrong. */ diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/NodeInfo.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/NodeInfo.kt index 4e88ddfe7..066e74c5f 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/NodeInfo.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/NodeInfo.kt @@ -200,6 +200,7 @@ data class NodeInfo( var channel: Int = 0, var environmentMetrics: EnvironmentMetrics? = null, var hopsAway: Int = 0, + var nodeStatus: String? = null, ) : Parcelable { @Suppress("MagicNumber") diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt index a35c49511..400438248 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt @@ -68,3 +68,9 @@ fun Int.mpsToMph(): Float { val mph = this * MPS_TO_KMPH * KM_TO_MILES return mph } + +/** Returns true if this packet arrived via a LoRa transport mechanism. */ +fun MeshPacket.isLora(): Boolean = transport_mechanism == MeshPacket.TransportMechanism.TRANSPORT_LORA || + transport_mechanism == MeshPacket.TransportMechanism.TRANSPORT_LORA_ALT1 || + transport_mechanism == MeshPacket.TransportMechanism.TRANSPORT_LORA_ALT2 || + transport_mechanism == MeshPacket.TransportMechanism.TRANSPORT_LORA_ALT3 diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt index 63a3d7172..7b8c4b170 100644 --- a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt +++ b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt @@ -87,13 +87,13 @@ class ServiceRepository @Inject constructor() { _errorMessage.value = null } - private val _statusMessage = MutableStateFlow(null) - val statusMessage: StateFlow - get() = _statusMessage + private val _connectionProgress = MutableStateFlow(null) + val connectionProgress: StateFlow + get() = _connectionProgress - fun setStatusMessage(text: String) { + fun setConnectionProgress(text: String) { if (connectionState.value != ConnectionState.Connected) { - _statusMessage.value = text + _connectionProgress.value = text } } diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml index 120aa7680..dc79004fc 100644 --- a/core/strings/src/commonMain/composeResources/values/strings.xml +++ b/core/strings/src/commonMain/composeResources/values/strings.xml @@ -48,6 +48,9 @@ Last heard via MQTT via MQTT + via UDP + via API + Internal via Favorite Only show ignored Nodes Unrecognized diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/TransportIcon.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/TransportIcon.kt new file mode 100644 index 000000000..387b44213 --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/TransportIcon.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025-2026 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 . + */ +package org.meshtastic.core.ui.component + +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import org.jetbrains.compose.resources.stringResource +import org.meshtastic.core.strings.Res +import org.meshtastic.core.strings.internal +import org.meshtastic.core.strings.via_api +import org.meshtastic.core.strings.via_mqtt +import org.meshtastic.core.strings.via_udp +import org.meshtastic.core.ui.icon.Api +import org.meshtastic.core.ui.icon.Cloud +import org.meshtastic.core.ui.icon.Device +import org.meshtastic.core.ui.icon.MeshtasticIcons +import org.meshtastic.core.ui.icon.Udp +import org.meshtastic.proto.MeshPacket + +@Composable +fun TransportIcon(transport: Int, viaMqtt: Boolean, modifier: Modifier = Modifier) { + val (icon, description) = + when { + viaMqtt || transport == MeshPacket.TransportMechanism.TRANSPORT_MQTT.value -> + MeshtasticIcons.Cloud to stringResource(Res.string.via_mqtt) + transport == MeshPacket.TransportMechanism.TRANSPORT_MULTICAST_UDP.value -> + MeshtasticIcons.Udp to stringResource(Res.string.via_udp) + transport == MeshPacket.TransportMechanism.TRANSPORT_API.value -> + MeshtasticIcons.Api to stringResource(Res.string.via_api) + transport == MeshPacket.TransportMechanism.TRANSPORT_INTERNAL.value -> + MeshtasticIcons.Device to stringResource(Res.string.internal) + else -> return + } + Icon(icon, contentDescription = description, modifier = modifier) +} diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Status.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Status.kt index efa5999e9..a0f02f209 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Status.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Status.kt @@ -28,10 +28,13 @@ import androidx.compose.material.icons.rounded.Cloud import androidx.compose.material.icons.rounded.CloudOff import androidx.compose.material.icons.rounded.Dangerous import androidx.compose.material.icons.rounded.History +import androidx.compose.material.icons.rounded.Lan import androidx.compose.material.icons.rounded.NoCell +import androidx.compose.material.icons.rounded.SettingsEthernet import androidx.compose.material.icons.rounded.SpeakerNotesOff import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarBorder +import androidx.compose.material.icons.rounded.Terminal import androidx.compose.material.icons.twotone.Cloud import androidx.compose.material.icons.twotone.CloudDone import androidx.compose.material.icons.twotone.CloudOff @@ -84,3 +87,10 @@ val MeshtasticIcons.CheckCircle: ImageVector val MeshtasticIcons.Acknowledged: ImageVector get() = Icons.TwoTone.HowToReg + +val MeshtasticIcons.Udp: ImageVector + get() = Icons.Rounded.Lan +val MeshtasticIcons.Api: ImageVector + get() = Icons.Rounded.Terminal +val MeshtasticIcons.Ethernet: ImageVector + get() = Icons.Rounded.SettingsEthernet diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt index 5c6ba9f4d..e29c19d9b 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt @@ -70,15 +70,14 @@ import org.meshtastic.core.strings.filter_message_label import org.meshtastic.core.strings.message_delivery_status import org.meshtastic.core.strings.reply import org.meshtastic.core.strings.sample_message -import org.meshtastic.core.strings.via_mqtt import org.meshtastic.core.ui.component.AutoLinkText import org.meshtastic.core.ui.component.NodeChip import org.meshtastic.core.ui.component.Rssi import org.meshtastic.core.ui.component.Snr +import org.meshtastic.core.ui.component.TransportIcon import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider import org.meshtastic.core.ui.emoji.EmojiPicker import org.meshtastic.core.ui.icon.Acknowledged -import org.meshtastic.core.ui.icon.Cloud import org.meshtastic.core.ui.icon.CloudDone import org.meshtastic.core.ui.icon.CloudOffTwoTone import org.meshtastic.core.ui.icon.CloudSync @@ -239,13 +238,11 @@ internal fun MessageItem( maxLines = 1, style = MaterialTheme.typography.labelMedium, ) - if (message.viaMqtt) { - Icon( - MeshtasticIcons.Cloud, - contentDescription = stringResource(Res.string.via_mqtt), - modifier = Modifier.size(16.dp), - ) - } + TransportIcon( + transport = message.transportMechanism, + viaMqtt = message.viaMqtt, + modifier = Modifier.size(16.dp), + ) } } Surface( @@ -292,7 +289,7 @@ internal fun MessageItem( Row(modifier = Modifier, verticalAlignment = Alignment.CenterVertically) { if (!message.fromLocal) { - if (message.hopsAway == 0) { + if (message.hopsAway == 0 && !message.viaMqtt) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Snr(message.snr) Rssi(message.rssi) @@ -309,7 +306,12 @@ internal fun MessageItem( tint = cardColors.contentColor.copy(alpha = 0.7f), ) Text( - text = message.hopsAway.toString(), + text = + if (message.hopsAway >= 0) { + message.hopsAway.toString() + } else { + "?" + }, style = MaterialTheme.typography.labelSmall, ) } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt index 2d0b974d0..c60023f05 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt @@ -73,6 +73,7 @@ fun DeviceDetailsSection(state: MetricsState, modifier: Modifier = Modifier) { Spacer(modifier = Modifier.height(16.dp)) SectionDivider() + val deviceText = state.reportedTarget?.let { target -> "${deviceHardware.displayName} ($target)" } ?: deviceHardware.displayName diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt index fbab30878..13183336f 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt @@ -14,6 +14,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +@file:Suppress("TooManyFunctions") + package org.meshtastic.feature.node.component import android.content.ClipData @@ -30,6 +32,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Notes import androidx.compose.material.icons.rounded.Numbers import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -48,6 +51,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource @@ -69,10 +73,12 @@ import org.meshtastic.core.strings.role import org.meshtastic.core.strings.rssi import org.meshtastic.core.strings.short_name import org.meshtastic.core.strings.snr +import org.meshtastic.core.strings.status_message import org.meshtastic.core.strings.supported import org.meshtastic.core.strings.uptime import org.meshtastic.core.strings.user_id import org.meshtastic.core.strings.via_mqtt +import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider import org.meshtastic.core.ui.icon.ArrowCircleUp import org.meshtastic.core.ui.icon.ChannelUtilization import org.meshtastic.core.ui.icon.Cloud @@ -84,6 +90,7 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Person import org.meshtastic.core.ui.icon.Role import org.meshtastic.core.ui.icon.Verified +import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.util.formatAgo @Composable @@ -135,13 +142,17 @@ private fun MismatchKeyWarning(modifier: Modifier = Modifier) { private fun MainNodeDetails(node: Node) { Column { NameAndRoleRow(node) + node.nodeStatus?.let { status -> + SectionDivider() + StatusMessageRow(status) + } SectionDivider() NodeIdentificationRow(node) SectionDivider() HearsAndHopsRow(node) SectionDivider() UserAndUptimeRow(node) - if (node.hopsAway == 0) { + if (node.hopsAway == 0 && !node.viaMqtt) { SectionDivider() SignalRow(node) } @@ -175,6 +186,16 @@ private fun NameAndRoleRow(node: Node) { } } +@Composable +private fun StatusMessageRow(status: String) { + InfoItem( + label = stringResource(Res.string.status_message), + value = status, + icon = Icons.AutoMirrored.Rounded.Notes, + modifier = Modifier.fillMaxWidth(), + ) +} + @Composable private fun NodeIdentificationRow(node: Node) { Row(modifier = Modifier.fillMaxWidth()) { @@ -352,3 +373,12 @@ private fun PublicKeyItem(publicKeyBytes: ByteArray) { ) } } + +@PreviewLightDark +@Composable +private fun NodeDetailsSectionPreview() { + AppTheme { + val node = NodePreviewParameterProvider().values.last().copy(nodeStatus = "Going to the farm.. to grow wheat.") + NodeDetailsSection(node = node) + } +} diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt index 3e7c65fcc..1eabb0145 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeItem.kt @@ -30,9 +30,12 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Notes import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor @@ -84,6 +87,7 @@ import org.meshtastic.core.ui.component.Snr import org.meshtastic.core.ui.component.SoilMoistureInfo import org.meshtastic.core.ui.component.SoilTemperatureInfo import org.meshtastic.core.ui.component.TemperatureInfo +import org.meshtastic.core.ui.component.TransportIcon import org.meshtastic.core.ui.component.determineSignalQuality import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider import org.meshtastic.core.ui.icon.AirUtilization @@ -171,6 +175,28 @@ fun NodeItem( contentColor = contentColor, ) + thatNode.nodeStatus?.let { status -> + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Notes, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = contentColor.copy(alpha = 0.7f), + ) + Text( + text = status, + style = MaterialTheme.typography.bodyMedium, + color = contentColor, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + NodeBatteryPositionRow( thatNode = thatNode, distance = distance, @@ -252,7 +278,7 @@ private fun NodeSignalRow(thatNode: Node, isThisNode: Boolean, contentColor: Col Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { if (thatNode.hopsAway > 0) { HopsInfo(hops = thatNode.hopsAway, contentColor = contentColor) - } else { + } else if (thatNode.hopsAway == 0 && !thatNode.viaMqtt) { Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, @@ -395,13 +421,21 @@ private fun NodeItemHeader( ) Column(modifier = Modifier.weight(1f)) { - Text( - text = longName, - style = MaterialTheme.typography.titleMediumEmphasized.copy(fontStyle = style), - textDecoration = TextDecoration.LineThrough.takeIf { isIgnored }, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = longName, + style = MaterialTheme.typography.titleMediumEmphasized.copy(fontStyle = style), + textDecoration = TextDecoration.LineThrough.takeIf { isIgnored }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), + ) + TransportIcon( + transport = thatNode.lastTransport, + viaMqtt = thatNode.viaMqtt, + modifier = Modifier.size(16.dp), + ) + } LastHeardInfo(lastHeard = thatNode.lastHeard, showLabel = false, contentColor = contentColor) } @@ -439,6 +473,17 @@ fun NodeInfoSimplePreview() { } } +@Composable +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +fun NodeInfoStatusPreview() { + AppTheme { + val thisNode = NodePreviewParameterProvider().values.first() + val thatNode = + NodePreviewParameterProvider().values.last().copy(nodeStatus = "Going to the farm.. to grow wheat.") + NodeItem(thisNode = thisNode, thatNode = thatNode, 0, true, connectionState = ConnectionState.Connected) + } +} + @Composable @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) fun NodeInfoSignalPreview() { diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailList.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailList.kt deleted file mode 100644 index d8ac5f1af..000000000 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailList.kt +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (c) 2025-2026 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 . - */ -package org.meshtastic.feature.node.detail - -import android.Manifest -import android.content.Intent -import android.provider.Settings -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.meshtastic.core.database.entity.FirmwareRelease -import org.meshtastic.core.database.model.Node -import org.meshtastic.core.ui.component.SharedContactDialog -import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider -import org.meshtastic.core.ui.theme.AppTheme -import org.meshtastic.feature.node.compass.CompassUiState -import org.meshtastic.feature.node.compass.CompassViewModel -import org.meshtastic.feature.node.component.AdministrationSection -import org.meshtastic.feature.node.component.CompassSheetContent -import org.meshtastic.feature.node.component.DeviceActions -import org.meshtastic.feature.node.component.DeviceDetailsSection -import org.meshtastic.feature.node.component.FirmwareReleaseSheetContent -import org.meshtastic.feature.node.component.NodeDetailsSection -import org.meshtastic.feature.node.component.NodeMenuAction -import org.meshtastic.feature.node.component.NotesSection -import org.meshtastic.feature.node.component.PositionSection -import org.meshtastic.feature.node.model.LogsType -import org.meshtastic.feature.node.model.MetricsState -import org.meshtastic.feature.node.model.NodeDetailAction - -@Composable -fun NodeDetailContent( - node: Node, - ourNode: Node?, - metricsState: MetricsState, - lastTracerouteTime: Long?, - lastRequestNeighborsTime: Long?, - availableLogs: Set, - onAction: (NodeDetailAction) -> Unit, - onSaveNotes: (nodeNum: Int, notes: String) -> Unit, - modifier: Modifier = Modifier, -) { - var showShareDialog by remember { mutableStateOf(false) } - if (showShareDialog) { - SharedContactDialog(node) { showShareDialog = false } - } - - NodeDetailList( - node = node, - lastTracerouteTime = lastTracerouteTime, - lastRequestNeighborsTime = lastRequestNeighborsTime, - ourNode = ourNode, - metricsState = metricsState, - onAction = { action -> - if (action is NodeDetailAction.ShareContact) { - showShareDialog = true - } else { - onAction(action) - } - }, - modifier = modifier, - availableLogs = availableLogs, - onSaveNotes = onSaveNotes, - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -@Suppress("LongMethod") -fun NodeDetailList( - node: Node, - lastTracerouteTime: Long?, - lastRequestNeighborsTime: Long?, - ourNode: Node?, - metricsState: MetricsState, - onAction: (NodeDetailAction) -> Unit, - availableLogs: Set, - onSaveNotes: (Int, String) -> Unit, - modifier: Modifier = Modifier, -) { - var showFirmwareSheet by remember { mutableStateOf(false) } - var selectedFirmware by remember { mutableStateOf(null) } - var showCompassSheet by remember { mutableStateOf(false) } - - val inspectionMode = LocalInspectionMode.current - val compassViewModel = if (inspectionMode) null else hiltViewModel() - val compassUiState by - compassViewModel?.uiState?.collectAsStateWithLifecycle() ?: remember { mutableStateOf(CompassUiState()) } - var compassTargetNode by remember { mutableStateOf(null) } - - val permissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { _ -> } - val locationSettingsLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> } - - FirmwareSheetHost( - showFirmwareSheet = showFirmwareSheet, - onDismiss = { showFirmwareSheet = false }, - firmwareRelease = selectedFirmware, - ) - - CompassSheetHost( - showCompassSheet = showCompassSheet, - compassViewModel = compassViewModel, - compassUiState = compassUiState, - onDismiss = { showCompassSheet = false }, - permissionLauncher = permissionLauncher, - locationSettingsLauncher = locationSettingsLauncher, - onRequestPosition = { - compassTargetNode?.let { target -> - onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestPosition(target))) - } - }, - ) - - Column( - modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp).focusable(), - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { - NodeDetailsSection(node) - - DeviceActions( - isLocal = metricsState.isLocal, - lastTracerouteTime = lastTracerouteTime, - lastRequestNeighborsTime = lastRequestNeighborsTime, - node = node, - availableLogs = availableLogs, - onAction = onAction, - metricsState = metricsState, - ) - - PositionSection( - node = node, - ourNode = ourNode, - metricsState = metricsState, - availableLogs = availableLogs, - onAction = { action -> - when (action) { - is NodeDetailAction.OpenCompass -> { - compassViewModel?.start(action.node, action.displayUnits) - compassTargetNode = action.node - showCompassSheet = compassViewModel != null - } - - else -> onAction(action) - } - }, - ) - - if (metricsState.deviceHardware != null) { - DeviceDetailsSection(metricsState) - } - - NotesSection(node = node, onSaveNotes = onSaveNotes) - - if (!metricsState.isManaged) { - AdministrationSection( - node = node, - metricsState = metricsState, - onAction = onAction, - onFirmwareSelect = { firmware -> - selectedFirmware = firmware - showFirmwareSheet = true - }, - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun FirmwareSheetHost(showFirmwareSheet: Boolean, onDismiss: () -> Unit, firmwareRelease: FirmwareRelease?) { - if (showFirmwareSheet) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) - ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { - firmwareRelease?.let { FirmwareReleaseSheetContent(firmwareRelease = it) } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -@Suppress("LongParameterList") -private fun CompassSheetHost( - showCompassSheet: Boolean, - compassViewModel: CompassViewModel?, - compassUiState: CompassUiState, - onDismiss: () -> Unit, - permissionLauncher: ManagedActivityResultLauncher, Map>, - locationSettingsLauncher: ManagedActivityResultLauncher, - onRequestPosition: () -> Unit, -) { - if (showCompassSheet && compassViewModel != null) { - DisposableEffect(Unit) { onDispose { compassViewModel.stop() } } - - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) - ModalBottomSheet( - onDismissRequest = { - compassViewModel.stop() - onDismiss() - }, - sheetState = sheetState, - ) { - CompassSheetContent( - uiState = compassUiState, - onRequestLocationPermission = { - permissionLauncher.launch( - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), - ) - }, - onOpenLocationSettings = { - locationSettingsLauncher.launch(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) - }, - onRequestPosition = onRequestPosition, - modifier = Modifier.padding(bottom = 24.dp), - ) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun NodeDetailsPreview(@PreviewParameter(NodePreviewParameterProvider::class) node: Node) { - AppTheme { - NodeDetailList( - node = node, - ourNode = node, - lastTracerouteTime = null, - lastRequestNeighborsTime = null, - metricsState = MetricsState.Companion.Empty, - availableLogs = emptySet(), - onAction = {}, - onSaveNotes = { _, _ -> }, - ) - } -} diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt index ec2856fcc..f72d376fe 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreen.kt @@ -52,6 +52,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -61,9 +63,12 @@ import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.model.Node import org.meshtastic.core.navigation.Route import org.meshtastic.core.strings.Res +import org.meshtastic.core.strings.details import org.meshtastic.core.strings.loading import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.component.SharedContactDialog +import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider +import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.feature.node.compass.CompassUiState import org.meshtastic.feature.node.compass.CompassViewModel import org.meshtastic.feature.node.component.AdministrationSection @@ -75,6 +80,7 @@ import org.meshtastic.feature.node.component.NodeDetailsSection import org.meshtastic.feature.node.component.NodeMenuAction import org.meshtastic.feature.node.component.NotesSection import org.meshtastic.feature.node.component.PositionSection +import org.meshtastic.feature.node.model.MetricsState import org.meshtastic.feature.node.model.NodeDetailAction private sealed interface NodeDetailOverlay { @@ -143,7 +149,8 @@ private fun NodeDetailScaffold( modifier = modifier, topBar = { MainAppBar( - title = node?.user?.long_name ?: "", + title = getString(Res.string.details), + subtitle = node?.user?.long_name ?: "", ourNode = uiState.ourNode, showNodeChip = false, canNavigateUp = true, @@ -343,3 +350,26 @@ private fun handleNodeAction( else -> {} } } + +@Preview(showBackground = true) +@Composable +private fun NodeDetailListPreview(@PreviewParameter(NodePreviewParameterProvider::class) node: Node) { + AppTheme { + val uiState = + NodeDetailUiState( + node = node, + ourNode = node, + metricsState = MetricsState(node = node, isLocal = true, isManaged = false), + availableLogs = emptySet(), + ) + NodeDetailList( + node = node, + ourNode = node, + uiState = uiState, + listState = rememberLazyListState(), + onAction = {}, + onFirmwareSelect = {}, + onSaveNotes = { _, _ -> }, + ) + } +} diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt index f76dc1cd2..7f9be54c3 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt @@ -46,6 +46,7 @@ import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.database.model.Node import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MyNodeInfo +import org.meshtastic.core.model.util.isLora import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.service.ServiceRepository @@ -216,7 +217,13 @@ constructor( deviceMetrics = data.telemetry.filter { it.device_metrics != null }, powerMetrics = data.telemetry.filter { it.power_metrics != null }, hostMetrics = data.telemetry.filter { it.host_metrics != null }, - signalMetrics = data.packets.filter { (it.rx_time ?: 0) > 0 }, + signalMetrics = + data.packets.filter { pkt -> + (pkt.rx_time ?: 0) > 0 && + pkt.hop_start == pkt.hop_limit && + pkt.via_mqtt != true && + pkt.isLora() + }, positionLogs = data.positionPackets.mapNotNull { it.toPosition() }, paxMetrics = data.paxLogs, tracerouteRequests = data.tracerouteRequests,