diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index f43cb4049..000000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 88e766d63..27092f63a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -184,7 +184,7 @@ dependencies {
kspAndroidTest "com.google.dagger:hilt-compiler:$hilt_version"
// Navigation
- def nav_version = "2.8.2"
+ def nav_version = "2.8.4"
implementation "androidx.navigation:navigation-compose:$nav_version"
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
diff --git a/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/15.json b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/15.json
new file mode 100644
index 000000000..e9557798e
--- /dev/null
+++ b/app/schemas/com.geeksville.mesh.database.MeshtasticDatabase/15.json
@@ -0,0 +1,522 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 15,
+ "identityHash": "2435abd7894404b70957f327189b0de7",
+ "entities": [
+ {
+ "tableName": "my_node",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, PRIMARY KEY(`myNodeNum`))",
+ "fields": [
+ {
+ "fieldPath": "myNodeNum",
+ "columnName": "myNodeNum",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "model",
+ "columnName": "model",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "firmwareVersion",
+ "columnName": "firmwareVersion",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "couldUpdate",
+ "columnName": "couldUpdate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shouldUpdate",
+ "columnName": "shouldUpdate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currentPacketId",
+ "columnName": "currentPacketId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageTimeoutMsec",
+ "columnName": "messageTimeoutMsec",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "minAppVersion",
+ "columnName": "minAppVersion",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxChannels",
+ "columnName": "maxChannels",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasWifi",
+ "columnName": "hasWifi",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "myNodeNum"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "nodes",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `environment_metrics` BLOB NOT NULL, `power_metrics` BLOB NOT NULL, `paxcounter` BLOB NOT NULL, PRIMARY KEY(`num`))",
+ "fields": [
+ {
+ "fieldPath": "num",
+ "columnName": "num",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "user",
+ "columnName": "user",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "longName",
+ "columnName": "long_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "shortName",
+ "columnName": "short_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "latitude",
+ "columnName": "latitude",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "longitude",
+ "columnName": "longitude",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "snr",
+ "columnName": "snr",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rssi",
+ "columnName": "rssi",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastHeard",
+ "columnName": "last_heard",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deviceTelemetry",
+ "columnName": "device_metrics",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "channel",
+ "columnName": "channel",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "viaMqtt",
+ "columnName": "via_mqtt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hopsAway",
+ "columnName": "hops_away",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isFavorite",
+ "columnName": "is_favorite",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isIgnored",
+ "columnName": "is_ignored",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "environmentTelemetry",
+ "columnName": "environment_metrics",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "powerTelemetry",
+ "columnName": "power_metrics",
+ "affinity": "BLOB",
+ "notNull": true
+ },
+ {
+ "fieldPath": "paxcounter",
+ "columnName": "paxcounter",
+ "affinity": "BLOB",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "num"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "packet",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `myNodeNum` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `read` INTEGER NOT NULL DEFAULT 1, `data` TEXT NOT NULL, `packet_id` INTEGER NOT NULL DEFAULT 0, `routing_error` INTEGER NOT NULL DEFAULT -1, `reply_id` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "myNodeNum",
+ "columnName": "myNodeNum",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "port_num",
+ "columnName": "port_num",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contact_key",
+ "columnName": "contact_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "received_time",
+ "columnName": "received_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "read",
+ "columnName": "read",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "data",
+ "columnName": "data",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "packetId",
+ "columnName": "packet_id",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "routingError",
+ "columnName": "routing_error",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "replyId",
+ "columnName": "reply_id",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "uuid"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_packet_myNodeNum",
+ "unique": false,
+ "columnNames": [
+ "myNodeNum"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_myNodeNum` ON `${TABLE_NAME}` (`myNodeNum`)"
+ },
+ {
+ "name": "index_packet_port_num",
+ "unique": false,
+ "columnNames": [
+ "port_num"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_port_num` ON `${TABLE_NAME}` (`port_num`)"
+ },
+ {
+ "name": "index_packet_contact_key",
+ "unique": false,
+ "columnNames": [
+ "contact_key"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_packet_contact_key` ON `${TABLE_NAME}` (`contact_key`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "contact_settings",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`contact_key` TEXT NOT NULL, `muteUntil` INTEGER NOT NULL, PRIMARY KEY(`contact_key`))",
+ "fields": [
+ {
+ "fieldPath": "contact_key",
+ "columnName": "contact_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "muteUntil",
+ "columnName": "muteUntil",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "contact_key"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "log",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message_type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "received_date",
+ "columnName": "received_date",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "raw_message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fromNum",
+ "columnName": "from_num",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "portNum",
+ "columnName": "port_num",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "fromRadio",
+ "columnName": "from_radio",
+ "affinity": "BLOB",
+ "notNull": true,
+ "defaultValue": "x''"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "uuid"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_log_from_num",
+ "unique": false,
+ "columnNames": [
+ "from_num"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)"
+ },
+ {
+ "name": "index_log_port_num",
+ "unique": false,
+ "columnNames": [
+ "port_num"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "quick_chat",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mode",
+ "columnName": "mode",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "uuid"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "reactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`reply_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `emoji` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`reply_id`, `user_id`, `emoji`))",
+ "fields": [
+ {
+ "fieldPath": "replyId",
+ "columnName": "reply_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "emoji",
+ "columnName": "emoji",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "reply_id",
+ "user_id",
+ "emoji"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_reactions_reply_id",
+ "unique": false,
+ "columnNames": [
+ "reply_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_reply_id` ON `${TABLE_NAME}` (`reply_id`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2435abd7894404b70957f327189b0de7')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/assets/device_hardware.json b/app/src/main/assets/device_hardware.json
new file mode 100644
index 000000000..853bec3b8
--- /dev/null
+++ b/app/src/main/assets/device_hardware.json
@@ -0,0 +1,764 @@
+[
+ {
+ "hwModel": 1,
+ "hwModelSlug": "TLORA_V2",
+ "platformioTarget": "tlora-v2",
+ "architecture": "esp32",
+ "activelySupported": false,
+ "displayName": "LILYGO T-LoRa V2",
+ "tags": [
+ "LilyGo"
+ ]
+ },
+ {
+ "hwModel": 2,
+ "hwModelSlug": "TLORA_V1",
+ "platformioTarget": "tlora-v1",
+ "architecture": "esp32",
+ "activelySupported": false,
+ "displayName": "LILYGO T-LoRa V1",
+ "tags": [
+ "LilyGo"
+ ]
+ },
+ {
+ "hwModel": 3,
+ "hwModelSlug": "TLORA_V2_1_1P6",
+ "platformioTarget": "tlora-v2-1-1_6",
+ "architecture": "esp32",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "LILYGO T-LoRa V2.1-1.6",
+ "tags": [
+ "LilyGo"
+ ],
+ "images": [
+ "tlora-v2-1-1_6.svg"
+ ]
+ },
+ {
+ "hwModel": 4,
+ "hwModelSlug": "TBEAM",
+ "platformioTarget": "tbeam",
+ "architecture": "esp32",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "LILYGO T-Beam",
+ "tags": [
+ "LilyGo"
+ ],
+ "images": [
+ "tbeam.svg"
+ ]
+ },
+ {
+ "hwModel": 5,
+ "hwModelSlug": "HELTEC_V2_0",
+ "platformioTarget": "heltec-v2_0",
+ "architecture": "esp32",
+ "activelySupported": false,
+ "displayName": "Heltec V2.0",
+ "tags": [
+ "Heltec"
+ ]
+ },
+ {
+ "hwModel": 6,
+ "hwModelSlug": "TBEAM_V0P7",
+ "platformioTarget": "tbeam0_7",
+ "architecture": "esp32",
+ "activelySupported": false,
+ "displayName": "LILYGO T-Beam V0.7",
+ "tags": [
+ "LilyGo"
+ ]
+ },
+ {
+ "hwModel": 7,
+ "hwModelSlug": "T_ECHO",
+ "platformioTarget": "t-echo",
+ "architecture": "nrf52840",
+ "supportLevel": 1,
+ "activelySupported": true,
+ "displayName": "LILYGO T-Echo",
+ "tags": [
+ "LilyGo"
+ ],
+ "images": [
+ "t-echo.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 8,
+ "hwModelSlug": "TLORA_V1_1P3",
+ "platformioTarget": "tlora-v1_3",
+ "architecture": "esp32",
+ "activelySupported": false,
+ "displayName": "LILYGO T-LoRa V1.1-1.3",
+ "tags": [
+ "LilyGo"
+ ]
+ },
+ {
+ "hwModel": 9,
+ "hwModelSlug": "RAK4631",
+ "platformioTarget": "rak4631",
+ "architecture": "nrf52840",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "RAK WisBlock 4631",
+ "tags": [
+ "RAK"
+ ],
+ "images": [
+ "rak4631.svg",
+ "rak4631_case.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 10,
+ "hwModelSlug": "HELTEC_V2_1",
+ "platformioTarget": "heltec-v2_1",
+ "architecture": "esp32",
+ "activelySupported": false,
+ "displayName": "Heltec V2.1",
+ "tags": [
+ "Heltec"
+ ]
+ },
+ {
+ "hwModel": 11,
+ "hwModelSlug": "HELTEC_V1",
+ "platformioTarget": "heltec-v1",
+ "architecture": "esp32",
+ "activelySupported": false,
+ "displayName": "Heltec V1",
+ "tags": [
+ "Heltec"
+ ]
+ },
+ {
+ "hwModel": 12,
+ "hwModelSlug": "TBEAM_S3_CORE",
+ "platformioTarget": "tbeam-s3-core",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "LILYGO T-Beam Supreme",
+ "tags": [
+ "LilyGo"
+ ],
+ "images": [
+ "tbeam-s3-core.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 13,
+ "hwModelSlug": "RAK11200",
+ "platformioTarget": "rak11200",
+ "architecture": "esp32",
+ "activelySupported": false,
+ "displayName": "RAK WisBlock 11200",
+ "tags": [
+ "RAK"
+ ]
+ },
+ {
+ "hwModel": 14,
+ "hwModelSlug": "NANO_G1",
+ "platformioTarget": "nano-g1",
+ "architecture": "esp32",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "Nano G1",
+ "tags": [
+ "B&Q"
+ ]
+ },
+ {
+ "hwModel": 15,
+ "hwModelSlug": "TLORA_V2_1_1P8",
+ "platformioTarget": "tlora-v2-1-1_8",
+ "architecture": "esp32",
+ "activelySupported": true,
+ "supportLevel": 2,
+ "displayName": "LILYGO T-LoRa V2.1-1.8",
+ "tags": [
+ "LilyGo",
+ "2.4G LoRA"
+ ],
+ "images": [
+ "tlora-v2-1-1_8.svg"
+ ]
+ },
+ {
+ "hwModel": 16,
+ "hwModelSlug": "TLORA_T3_S3",
+ "platformioTarget": "tlora-t3s3-v1",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "displayName": "LILYGO T-LoRa T3-S3",
+ "supportLevel": 1,
+ "tags": [
+ "LilyGo"
+ ],
+ "images": [
+ "tlora-t3s3-v1.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 16,
+ "hwModelSlug": "TLORA_T3_S3",
+ "platformioTarget": "tlora-t3s3-epaper",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "LILYGO T-LoRa T3-S3 E-Ink",
+ "tags": [
+ "LilyGo"
+ ],
+ "images": [
+ "tlora-t3s3-epaper.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 17,
+ "hwModelSlug": "NANO_G1_EXPLORER",
+ "platformioTarget": "nano-g1-explorer",
+ "architecture": "esp32",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "Nano G1 Explorer",
+ "tags": [
+ "B&Q"
+ ]
+ },
+ {
+ "hwModel": 18,
+ "hwModelSlug": "NANO_G2_ULTRA",
+ "platformioTarget": "nano-g2-ultra",
+ "architecture": "nrf52840",
+ "activelySupported": true,
+ "supportLevel": 2,
+ "displayName": "Nano G2 Ultra",
+ "tags": [
+ "B&Q"
+ ],
+ "requiresDfu": true,
+ "images": [
+ "nano-g2-ultra.svg"
+ ]
+ },
+ {
+ "hwModel": 21,
+ "hwModelSlug": "WIO_WM1110",
+ "platformioTarget": "wio-tracker-wm1110",
+ "architecture": "nrf52840",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Seeed Wio WM1110 Tracker",
+ "tags": [
+ "Seeed"
+ ],
+ "images": [
+ "wio-tracker-wm1110.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 25,
+ "hwModelSlug": "STATION_G1",
+ "platformioTarget": "station-g1",
+ "architecture": "esp32",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "Station G1",
+ "tags": [
+ "B&Q"
+ ]
+ },
+ {
+ "hwModel": 26,
+ "hwModelSlug": "RAK11310",
+ "platformioTarget": "rak11310",
+ "architecture": "rp2040",
+ "activelySupported": true,
+ "supportLevel": 2,
+ "displayName": "RAK WisBlock 11310",
+ "tags": [
+ "RAK"
+ ],
+ "images": [
+ "rak11310.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 29,
+ "hwModelSlug": "CANARYONE",
+ "platformioTarget": "canaryone",
+ "architecture": "nrf52840",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "Canary One",
+ "tags": [
+ "Canary"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 30,
+ "hwModelSlug": "RP2040_LORA",
+ "platformioTarget": "rp2040-lora",
+ "architecture": "rp2040",
+ "activelySupported": true,
+ "supportLevel": 2,
+ "displayName": "RP2040 LoRa",
+ "tags": [
+ "Waveshare"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 31,
+ "hwModelSlug": "STATION_G2",
+ "platformioTarget": "station-g2",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 2,
+ "displayName": "Station G2",
+ "tags": [
+ "B&Q"
+ ],
+ "requiresDfu": true,
+ "images": [
+ "station-g2.svg"
+ ]
+ },
+ {
+ "hwModel": 39,
+ "hwModelSlug": "DIY_V1",
+ "platformioTarget": "meshtastic-diy-v1",
+ "architecture": "esp32",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "DIY V1",
+ "tags": [
+ "DIY"
+ ],
+ "images": [
+ "diy.svg"
+ ]
+ },
+ {
+ "hwModel": 39,
+ "hwModelSlug": "HYDRA",
+ "platformioTarget": "hydra",
+ "architecture": "esp32",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "Hydra",
+ "tags": [
+ "DIY"
+ ]
+ },
+ {
+ "hwModel": 41,
+ "hwModelSlug": "DR_DEV",
+ "platformioTarget": "meshtastic-dr-dev",
+ "architecture": "esp32",
+ "activelySupported": false,
+ "displayName": "DR-DEV",
+ "tags": [
+ "DIY"
+ ]
+ },
+ {
+ "hwModel": 42,
+ "hwModelSlug": "M5STACK",
+ "platformioTarget": "m5stack-core",
+ "architecture": "esp32",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "M5 Stack",
+ "tags": [
+ "M5Stack"
+ ]
+ },
+ {
+ "hwModel": 43,
+ "hwModelSlug": "HELTEC_V3",
+ "platformioTarget": "heltec-v3",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Heltec V3",
+ "tags": [
+ "Heltec"
+ ],
+ "images": [
+ "heltec-v3.svg",
+ "heltec-v3-case.svg"
+ ]
+ },
+ {
+ "hwModel": 44,
+ "hwModelSlug": "HELTEC_WSL_V3",
+ "platformioTarget": "heltec-wsl-v3",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Heltec Wireless Stick Lite V3",
+ "tags": [
+ "Heltec"
+ ],
+ "images": [
+ "heltec-wsl-v3.svg"
+ ]
+ },
+ {
+ "hwModel": 47,
+ "hwModelSlug": "RPI_PICO",
+ "platformioTarget": "pico",
+ "architecture": "rp2040",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "Raspberry Pi Pico",
+ "tags": [
+ "RPi",
+ "DIY"
+ ],
+ "requiresDfu": true,
+ "images": [
+ "pico.svg"
+ ]
+ },
+ {
+ "hwModel": 47,
+ "hwModelSlug": "RPI_PICO",
+ "platformioTarget": "picow",
+ "architecture": "rp2040",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "Raspberry Pi Pico W",
+ "tags": [
+ "RPi",
+ "DIY"
+ ],
+ "requiresDfu": true,
+ "images": [
+ "rpipicow.svg"
+ ]
+ },
+ {
+ "hwModel": 48,
+ "hwModelSlug": "HELTEC_WIRELESS_TRACKER",
+ "platformioTarget": "heltec-wireless-tracker",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Heltec Wireless Tracker V1.1",
+ "tags": [
+ "Heltec"
+ ],
+ "images": [
+ "heltec-wireless-tracker.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 58,
+ "hwModelSlug": "HELTEC_WIRELESS_TRACKER_V1_0",
+ "platformioTarget": "heltec-wireless-tracker-V1-0",
+ "architecture": "esp32-s3",
+ "activelySupported": false,
+ "supportLevel": 3,
+ "displayName": "Heltec Wireless Tracker V1.0",
+ "images": [
+ "heltec-wireless-tracker.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 49,
+ "hwModelSlug": "HELTEC_WIRELESS_PAPER",
+ "platformioTarget": "heltec-wireless-paper",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Heltec Wireless Paper",
+ "tags": [
+ "Heltec"
+ ],
+ "images": [
+ "heltec-wireless-paper.svg"
+ ]
+ },
+ {
+ "hwModel": 50,
+ "hwModelSlug": "T_DECK",
+ "platformioTarget": "t-deck",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "LILYGO T-Deck",
+ "tags": [
+ "LilyGo"
+ ],
+ "images": [
+ "t-deck.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 51,
+ "hwModelSlug": "T_WATCH_S3",
+ "platformioTarget": "t-watch-s3",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "LILYGO T-Watch S3",
+ "tags": [
+ "LilyGo"
+ ],
+ "images": [
+ "t-watch-s3.svg"
+ ]
+ },
+ {
+ "hwModel": 52,
+ "hwModelSlug": "PICOMPUTER_S3",
+ "platformioTarget": "picomputer-s3",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "Pi Computer S3"
+ },
+ {
+ "hwModel": 53,
+ "hwModelSlug": "HELTEC_HT62",
+ "platformioTarget": "heltec-ht62-esp32c3-sx1262",
+ "architecture": "esp32-c3",
+ "supportLevel": 1,
+ "activelySupported": true,
+ "displayName": "Heltec HT62",
+ "tags": [
+ "Heltec"
+ ],
+ "images": [
+ "heltec-ht62-esp32c3-sx1262.svg"
+ ]
+ },
+ {
+ "hwModel": 57,
+ "hwModelSlug": "HELTEC_WIRELESS_PAPER_V1_0",
+ "platformioTarget": "heltec-wireless-paper-v1_0",
+ "architecture": "esp32-s3",
+ "activelySupported": false,
+ "supportLevel": 3,
+ "tags": [
+ "Heltec"
+ ],
+ "displayName": "Heltec Wireless Paper V1.0",
+ "images": [
+ "heltec-wireless-paper-v1_0.svg"
+ ]
+ },
+ {
+ "hwModel": 59,
+ "hwModelSlug": "UNPHONE",
+ "platformioTarget": "unphone",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "unPhone",
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 48,
+ "hwModelSlug": "HELTEC_WIRELESS_TRACKER",
+ "platformioTarget": "tracksenger",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "TrackSenger (small TFT)",
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 48,
+ "hwModelSlug": "HELTEC_WIRELESS_TRACKER",
+ "platformioTarget": "tracksenger-lcd",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "TrackSenger (big TFT)",
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 48,
+ "hwModelSlug": "HELTEC_WIRELESS_TRACKER",
+ "platformioTarget": "tracksenger-oled",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "TrackSenger (big OLED)"
+ },
+ {
+ "hwModel": 61,
+ "hwModelSlug": "CDEBYTE_EORA_S3",
+ "platformioTarget": "CDEBYTE_EoRa-S3",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 3,
+ "displayName": "EBYTE EoRa-S3",
+ "tags": [
+ "EByte"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 64,
+ "hwModelSlug": "RADIOMASTER_900_BANDIT_NANO",
+ "platformioTarget": "radiomaster_900_bandit_nano",
+ "architecture": "esp32",
+ "activelySupported": true,
+ "supportLevel": 2,
+ "displayName": "RadioMaster 900 Bandit Nano",
+ "tags": [
+ "RadioMaster"
+ ]
+ },
+ {
+ "hwModel": 66,
+ "hwModelSlug": "HELTEC_VISION_MASTER_T190",
+ "platformioTarget": "heltec-vision-master-t190",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Heltec Vision Master T190",
+ "tags": [
+ "Heltec"
+ ],
+ "images": [
+ "heltec-vision-master-t190.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 67,
+ "hwModelSlug": "HELTEC_VISION_MASTER_E213",
+ "platformioTarget": "heltec-vision-master-e213",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Heltec Vision Master E213",
+ "tags": [
+ "Heltec"
+ ],
+ "images": [
+ "heltec-vision-master-e213.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 68,
+ "hwModelSlug": "HELTEC_VISION_MASTER_E290",
+ "platformioTarget": "heltec-vision-master-e290",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Heltec Vision Master E290",
+ "tags": [
+ "Heltec"
+ ],
+ "images": [
+ "heltec-vision-master-e290.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 69,
+ "hwModelSlug": "HELTEC_MESH_NODE_T114",
+ "platformioTarget": "heltec-mesh-node-t114",
+ "architecture": "nrf52840",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Heltec Mesh Node T114",
+ "tags": [
+ "Heltec"
+ ],
+ "images": [
+ "heltec-mesh-node-t114.svg",
+ "heltec-mesh-node-t114-case.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 70,
+ "hwModelSlug": "SENSECAP_INDICATOR",
+ "platformioTarget": "seeed-sensecap-indicator",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Seeed SenseCAP Indicator",
+ "tags": [
+ "Seeed"
+ ],
+ "images": [
+ "seeed-sensecap-indicator.svg"
+ ]
+ },
+ {
+ "hwModel": 71,
+ "hwModelSlug": "TRACKER_T1000_E",
+ "platformioTarget": "tracker-t1000-e",
+ "architecture": "nrf52840",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Seeed Card Tracker T1000-E",
+ "tags": [
+ "Seeed"
+ ],
+ "images": [
+ "tracker-t1000-e.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 72,
+ "hwModelSlug": "Seeed_XIAO_S3",
+ "platformioTarget": "seeed-xiao-s3",
+ "architecture": "esp32-s3",
+ "activelySupported": true,
+ "supportLevel": 1,
+ "displayName": "Seeed Xiao ESP32-S3",
+ "tags": [
+ "Seeed"
+ ],
+ "images": [
+ "seeed-xiao-s3.svg"
+ ],
+ "requiresDfu": true
+ },
+ {
+ "hwModel": 84,
+ "hwModelSlug": "WISMESH_TAP",
+ "platformioTarget": "rak_wismeshtap",
+ "architecture": "nrf52840",
+ "activelySupported": false,
+ "supportLevel": 1,
+ "displayName": "RAK WisMesh Tap",
+ "tags": [
+ "RAK"
+ ],
+ "images": [
+ "rak-wismeshtap.svg"
+ ],
+ "requiresDfu": true
+ }
+]
diff --git a/app/src/main/java/com/geeksville/mesh/DataPacket.kt b/app/src/main/java/com/geeksville/mesh/DataPacket.kt
index 7d8658412..4753358be 100644
--- a/app/src/main/java/com/geeksville/mesh/DataPacket.kt
+++ b/app/src/main/java/com/geeksville/mesh/DataPacket.kt
@@ -190,7 +190,7 @@ data class DataPacket(
const val PKC_CHANNEL_INDEX = 8
fun nodeNumToDefaultId(n: Int): String = "!%08x".format(n)
- fun idToDefaultNodeNum(id: String?): Int? = id?.toLong(16)?.toInt()
+ fun idToDefaultNodeNum(id: String?): Int? = runCatching { id?.toLong(16)?.toInt() }.getOrNull()
override fun createFromParcel(parcel: Parcel): DataPacket {
return DataPacket(parcel)
diff --git a/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt b/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt
index 03da56143..01f84d489 100644
--- a/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt
+++ b/app/src/main/java/com/geeksville/mesh/database/MeshtasticDatabase.kt
@@ -59,8 +59,9 @@ import com.geeksville.mesh.database.entity.ReactionEntity
AutoMigration(from = 11, to = 12),
AutoMigration(from = 12, to = 13, spec = AutoMigration12to13::class),
AutoMigration(from = 13, to = 14),
+ AutoMigration(from = 14, to = 15),
],
- version = 14,
+ version = 15,
exportSchema = true,
)
@TypeConverters(Converters::class)
diff --git a/app/src/main/java/com/geeksville/mesh/database/NodeRepository.kt b/app/src/main/java/com/geeksville/mesh/database/NodeRepository.kt
index 7da648fe2..007f881c7 100644
--- a/app/src/main/java/com/geeksville/mesh/database/NodeRepository.kt
+++ b/app/src/main/java/com/geeksville/mesh/database/NodeRepository.kt
@@ -67,6 +67,12 @@ class NodeRepository @Inject constructor(
.conflate()
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
+ fun getNode(userId: String): NodeEntity = nodeDBbyNum.value.values.find { it.user.id == userId }
+ ?: NodeEntity(
+ num = DataPacket.idToDefaultNodeNum(userId) ?: 0,
+ user = getUser(userId),
+ )
+
fun getUser(nodeNum: Int): MeshProtos.User = getUser(DataPacket.nodeNumToDefaultId(nodeNum))
fun getUser(userId: String): MeshProtos.User =
diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt b/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt
index 8cc47c742..e0d27fceb 100644
--- a/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt
+++ b/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt
@@ -74,6 +74,9 @@ data class NodeEntity(
@ColumnInfo(name = "is_favorite")
var isFavorite: Boolean = false,
+ @ColumnInfo(name = "is_ignored", defaultValue = "0")
+ var isIgnored: Boolean = false,
+
@ColumnInfo(name = "environment_metrics", typeAffinity = ColumnInfo.BLOB)
var environmentTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.getDefaultInstance(),
diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt
index 3634826c0..c36b0c6cb 100644
--- a/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt
+++ b/app/src/main/java/com/geeksville/mesh/database/entity/Packet.kt
@@ -33,18 +33,18 @@ data class PacketEntity(
@Relation(entity = ReactionEntity::class, parentColumn = "packet_id", entityColumn = "reply_id")
val reactions: List = emptyList(),
) {
- suspend fun toMessage(getUser: suspend (userId: String?) -> User) = with(packet) {
+ suspend fun toMessage(getNode: suspend (userId: String?) -> NodeEntity) = with(packet) {
Message(
uuid = uuid,
receivedTime = received_time,
- user = getUser(data.from),
+ node = getNode(data.from),
text = data.text.orEmpty(),
time = getShortDateTime(data.time),
read = read,
status = data.status,
routingError = routingError,
packetId = packetId,
- emojis = reactions.toReaction(getUser),
+ emojis = reactions.toReaction(getNode),
)
}
}
@@ -101,14 +101,14 @@ data class ReactionEntity(
)
private suspend fun ReactionEntity.toReaction(
- getUser: suspend (userId: String?) -> User
+ getNode: suspend (userId: String?) -> NodeEntity
) = Reaction(
replyId = replyId,
- user = getUser(userId),
+ user = getNode(userId).user,
emoji = emoji,
timestamp = timestamp,
)
private suspend fun List.toReaction(
- getUser: suspend (userId: String?) -> User
-) = this.map { it.toReaction(getUser) }
+ getNode: suspend (userId: String?) -> NodeEntity
+) = this.map { it.toReaction(getNode) }
diff --git a/app/src/main/java/com/geeksville/mesh/model/DeviceHardware.kt b/app/src/main/java/com/geeksville/mesh/model/DeviceHardware.kt
new file mode 100644
index 000000000..eb137599e
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/model/DeviceHardware.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2024 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.model
+
+import com.geeksville.mesh.MeshProtos.HardwareModel
+import com.geeksville.mesh.R
+import kotlinx.serialization.Serializable
+
+data class DeviceHardware(
+ val hwModel: Int,
+ val hwModelSlug: String,
+ val architecture: String,
+ val activelySupported: Boolean,
+ val supportLevel: Int? = null,
+ val displayName: String,
+ val tags: List? = listOf(),
+ val image: Int,
+ val requiresDfu: Boolean? = null,
+)
+
+@Serializable
+data class DeviceHardwareDto(
+ val hwModel: Int,
+ val hwModelSlug: String,
+ val platformioTarget: String,
+ val architecture: String,
+ val activelySupported: Boolean,
+ val supportLevel: Int? = null,
+ val displayName: String,
+ val tags: List? = listOf(),
+ val images: List? = listOf(),
+ val requiresDfu: Boolean? = null,
+) {
+ fun toDeviceHardware() = DeviceHardware(
+ hwModel = hwModel,
+ hwModelSlug = hwModelSlug,
+ architecture = architecture,
+ activelySupported = activelySupported,
+ supportLevel = supportLevel,
+ displayName = displayName,
+ tags = tags,
+ image = HardwareModel.forNumber(hwModel).getDeviceVectorImage(),
+ requiresDfu = requiresDfu
+ )
+}
+
+@Suppress("CyclomaticComplexMethod")
+private fun HardwareModel.getDeviceVectorImage(): Int = when (this) {
+ HardwareModel.DIY_V1 -> R.drawable.hw_diy
+ HardwareModel.HELTEC_HT62 -> R.drawable.hw_heltec_ht62_esp32c3_sx1262
+ HardwareModel.HELTEC_MESH_NODE_T114 -> R.drawable.hw_heltec_mesh_node_t114
+ HardwareModel.HELTEC_V3 -> R.drawable.hw_heltec_v3
+ HardwareModel.HELTEC_VISION_MASTER_E213 -> R.drawable.hw_heltec_vision_master_e213
+ HardwareModel.HELTEC_VISION_MASTER_E290 -> R.drawable.hw_heltec_vision_master_e290
+ HardwareModel.HELTEC_VISION_MASTER_T190 -> R.drawable.hw_heltec_vision_master_t190
+ HardwareModel.HELTEC_WIRELESS_PAPER -> R.drawable.hw_heltec_wireless_paper
+ HardwareModel.HELTEC_WIRELESS_TRACKER -> R.drawable.hw_heltec_wireless_tracker
+ HardwareModel.HELTEC_WIRELESS_TRACKER_V1_0 -> R.drawable.hw_heltec_wireless_tracker_v1_0
+ HardwareModel.HELTEC_WSL_V3 -> R.drawable.hw_heltec_wsl_v3
+ HardwareModel.NANO_G2_ULTRA -> R.drawable.hw_nano_g2_ultra
+ HardwareModel.RPI_PICO -> R.drawable.hw_pico
+ HardwareModel.NRF52_PROMICRO_DIY -> R.drawable.hw_promicro
+ HardwareModel.RAK11310 -> R.drawable.hw_rak11310
+ HardwareModel.RAK4631 -> R.drawable.hw_rak4631
+ HardwareModel.RPI_PICO2 -> R.drawable.hw_rpipicow
+ HardwareModel.SENSECAP_INDICATOR -> R.drawable.hw_seeed_sensecap_indicator
+ HardwareModel.SEEED_XIAO_S3 -> R.drawable.hw_seeed_xiao_s3
+ HardwareModel.STATION_G2 -> R.drawable.hw_station_g2
+ HardwareModel.T_DECK -> R.drawable.hw_t_deck
+ HardwareModel.T_ECHO -> R.drawable.hw_t_echo
+ HardwareModel.T_WATCH_S3 -> R.drawable.hw_t_watch_s3
+ HardwareModel.TBEAM -> R.drawable.hw_tbeam
+ HardwareModel.LILYGO_TBEAM_S3_CORE -> R.drawable.hw_tbeam_s3_core
+ HardwareModel.TLORA_C6 -> R.drawable.hw_tlora_c6
+ HardwareModel.TLORA_T3_S3 -> R.drawable.hw_tlora_t3s3_v1
+ HardwareModel.TLORA_V2_1_1P6 -> R.drawable.hw_tlora_v2_1_1_6
+ HardwareModel.TLORA_V2_1_1P8 -> R.drawable.hw_tlora_v2_1_1_8
+ HardwareModel.TRACKER_T1000_E -> R.drawable.hw_tracker_t1000_e
+ HardwareModel.WIO_WM1110 -> R.drawable.hw_wio_tracker_wm1110
+ HardwareModel.WISMESH_TAP -> R.drawable.hw_rak_wismeshtap
+ else -> R.drawable.hw_unknown
+}
diff --git a/app/src/main/java/com/geeksville/mesh/model/Message.kt b/app/src/main/java/com/geeksville/mesh/model/Message.kt
index 61ef926fc..6dc98b8be 100644
--- a/app/src/main/java/com/geeksville/mesh/model/Message.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/Message.kt
@@ -18,9 +18,9 @@
package com.geeksville.mesh.model
import com.geeksville.mesh.MeshProtos.Routing
-import com.geeksville.mesh.MeshProtos.User
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R
+import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.Reaction
val Routing.Error.stringRes: Int
@@ -47,7 +47,7 @@ val Routing.Error.stringRes: Int
data class Message(
val uuid: Long,
val receivedTime: Long,
- val user: User,
+ val node: NodeEntity,
val text: String,
val time: String,
val read: Boolean,
diff --git a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
index 1a4be6f9f..a27d8a83b 100644
--- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
@@ -29,6 +29,7 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
import com.geeksville.mesh.CoroutineDispatchers
+import com.geeksville.mesh.MeshProtos.HardwareModel
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.Position
import com.geeksville.mesh.Portnums.PortNum
@@ -56,9 +57,11 @@ import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlinx.serialization.json.Json
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
+import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
@@ -75,6 +78,7 @@ data class MetricsState(
val tracerouteRequests: List = emptyList(),
val tracerouteResults: List = emptyList(),
val positionLogs: List = emptyList(),
+ val deviceHardware: DeviceHardware? = null,
) {
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty()
@@ -207,12 +211,21 @@ class MetricsViewModel @Inject constructor(
private val _timeFrame = MutableStateFlow(TimeFrame.TWENTY_FOUR_HOURS)
val timeFrame: StateFlow = _timeFrame
+ private var deviceHardwareList: List = listOf()
+
init {
@OptIn(ExperimentalCoroutinesApi::class)
radioConfigRepository.nodeDBbyNum
.mapLatest { nodes -> nodes[destNum] }
.distinctUntilChanged()
- .onEach { node -> _state.update { state -> state.copy(node = node) } }
+ .onEach { node ->
+ _state.update { state -> state.copy(node = node) }
+ node?.user?.hwModel?.let { hwModel ->
+ _state.update { state ->
+ state.copy(deviceHardware = getDeviceHardwareFromHardwareModel(hwModel))
+ }
+ }
+ }
.launchIn(viewModelScope)
radioConfigRepository.deviceProfileFlow.onEach { profile ->
@@ -316,4 +329,20 @@ class MetricsViewModel @Inject constructor(
errormsg("Can't write file error: ${ex.message}")
}
}
+
+ private fun getDeviceHardwareFromHardwareModel(
+ hwModel: HardwareModel
+ ): DeviceHardware? {
+ if (deviceHardwareList.isEmpty()) {
+ try {
+ val json =
+ app.assets.open("device_hardware.json").bufferedReader().use { it.readText() }
+ deviceHardwareList = Json.decodeFromString>(json)
+ .map { it.toDeviceHardware() }
+ } catch (ex: IOException) {
+ errormsg("Can't read device_hardware.json error: ${ex.message}")
+ }
+ }
+ return deviceHardwareList.find { it.hwModel == hwModel.number }
+ }
}
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index a8917719e..e07304dc1 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -142,7 +142,6 @@ data class NodesUiState(
val gpsFormat: Int = 0,
val distanceUnits: Int = 0,
val tempInFahrenheit: Boolean = false,
- val ignoreIncomingList: List = emptyList(),
val showDetails: Boolean = false,
) {
companion object {
@@ -227,7 +226,6 @@ class UIViewModel @Inject constructor(
gpsFormat = profile.config.display.gpsFormat.number,
distanceUnits = profile.config.display.units.number,
tempInFahrenheit = profile.moduleConfig.telemetry.environmentDisplayFahrenheit,
- ignoreIncomingList = profile.config.lora.ignoreIncomingList,
showDetails = showDetails,
)
}.stateIn(
@@ -255,6 +253,7 @@ class UIViewModel @Inject constructor(
get() = preferences.getInt(MAP_STYLE_ID, 0)
set(value) = preferences.edit { putInt(MAP_STYLE_ID, value) }
+ fun getNode(userId: String?) = nodeDB.getNode(userId ?: DataPacket.ID_BROADCAST)
fun getUser(userId: String?) = nodeDB.getUser(userId ?: DataPacket.ID_BROADCAST)
private val _snackbarText = MutableLiveData(null)
@@ -330,7 +329,7 @@ class UIViewModel @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
fun getMessagesFrom(contactKey: String) = packetRepository.getMessagesFrom(contactKey)
- .mapLatest { list -> list.map { it.toMessage(::getUser) } }
+ .mapLatest { list -> list.map { it.toMessage(::getNode) } }
@OptIn(ExperimentalCoroutinesApi::class)
val waypoints = packetRepository.getWaypoints().mapLatest { list ->
@@ -485,19 +484,11 @@ class UIViewModel @Inject constructor(
updateLoraConfig { it.copy { region = value } }
}
- fun ignoreNode(nodeNum: Int) = updateLoraConfig {
- it.copy {
- val list = ignoreIncoming.toMutableList().apply {
- if (contains(nodeNum)) {
- debug("removing node $nodeNum from ignore list")
- remove(nodeNum)
- } else {
- debug("adding node $nodeNum to ignore list")
- add(nodeNum)
- }
- }
- ignoreIncoming.clear()
- ignoreIncoming.addAll(list)
+ fun ignoreNode(node: NodeEntity) = viewModelScope.launch {
+ try {
+ radioConfigRepository.onServiceAction(ServiceAction.Ignore(node))
+ } catch (ex: RemoteException) {
+ errormsg("Ignore node error:", ex)
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index 2968785bb..f88f23d64 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -77,6 +77,7 @@ import javax.inject.Inject
import kotlin.math.absoluteValue
sealed class ServiceAction {
+ data class Ignore(val node: NodeEntity) : ServiceAction()
data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction()
}
@@ -303,6 +304,7 @@ class MeshService : Service(), Logging {
.launchIn(serviceScope)
radioConfigRepository.serviceAction.onEach { action ->
when (action) {
+ is ServiceAction.Ignore -> ignoreNode(action.node)
is ServiceAction.Reaction -> sendReaction(action)
}
}.launchIn(serviceScope)
@@ -1453,6 +1455,7 @@ class MeshService : Service(), Logging {
-1
}
it.isFavorite = info.isFavorite
+ it.isIgnored = info.isIgnored
}
}
@@ -1757,6 +1760,21 @@ class MeshService : Service(), Logging {
}
}
+ private fun ignoreNode(node: NodeEntity) = toRemoteExceptions {
+ sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket {
+ if (node.isIgnored) {
+ debug("removing node ${node.num} from ignore list")
+ removeIgnoredNode = node.num
+ } else {
+ debug("adding node ${node.num} to ignore list")
+ setIgnoredNode = node.num
+ }
+ })
+ updateNodeInfo(node.num) {
+ it.isIgnored = !node.isIgnored
+ }
+ }
+
private fun sendReaction(reaction: ServiceAction.Reaction) = toRemoteExceptions {
// contactKey: unique contact key filter (channel)+(nodeId)
val channel = reaction.contactKey[0].digitToInt()
diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
index 7072d9d58..8af5bc710 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
@@ -49,6 +49,7 @@ import androidx.compose.material.icons.twotone.Check
import androidx.compose.material.icons.twotone.Close
import androidx.compose.material.icons.twotone.ContentCopy
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
@@ -321,6 +322,11 @@ fun ChannelScreen(
channelSelections = channelSelections,
onClick = { showChannelEditor = true }
)
+ EditChannelUrl(
+ enabled = enabled,
+ channelUrl = selectedChannelSet.getChannelUrl(),
+ onConfirm = viewModel::requestChannelUrl
+ )
}
} else {
dragDropItemsIndexed(
@@ -354,14 +360,6 @@ fun ChannelScreen(
}
}
- item {
- EditChannelUrl(
- enabled = enabled,
- channelUrl = selectedChannelSet.getChannelUrl(),
- onConfirm = viewModel::requestChannelUrl
- )
- }
-
item {
DropDownPreference(title = stringResource(id = R.string.channel_options),
enabled = enabled,
@@ -419,6 +417,13 @@ private fun EditChannelUrl(
var valueState by remember(channelUrl) { mutableStateOf(channelUrl) }
var isError by remember { mutableStateOf(false) }
+ // Trigger dialog automatically when users paste a new valid URL
+ LaunchedEffect(valueState, isError) {
+ if (!isError && valueState != channelUrl) {
+ onConfirm(valueState)
+ }
+ }
+
OutlinedTextField(
value = valueState.toString(),
onValueChange = {
diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt
index fd179a690..ac7165273 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt
@@ -15,10 +15,12 @@
* along with this program. If not, see .
*/
-@file:Suppress("TooManyFunctions")
+@file:Suppress("TooManyFunctions", "LongMethod")
package com.geeksville.mesh.ui
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -36,9 +38,11 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
@@ -64,6 +68,7 @@ import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SignalCellularAlt
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Thermostat
+import androidx.compose.material.icons.filled.Verified
import androidx.compose.material.icons.filled.WaterDrop
import androidx.compose.material.icons.filled.Work
import androidx.compose.material.icons.outlined.Navigation
@@ -71,11 +76,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -98,8 +105,8 @@ import kotlin.math.ln
@Composable
fun NodeDetailScreen(
- viewModel: MetricsViewModel = hiltViewModel(),
modifier: Modifier = Modifier,
+ viewModel: MetricsViewModel = hiltViewModel(),
onNavigate: (Any) -> Unit,
) {
val state by viewModel.state.collectAsStateWithLifecycle()
@@ -124,15 +131,20 @@ fun NodeDetailScreen(
@Composable
private fun NodeDetailList(
+ modifier: Modifier = Modifier,
node: NodeEntity,
metricsState: MetricsState,
- modifier: Modifier = Modifier,
onNavigate: (Any) -> Unit = {},
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp),
) {
+ item {
+ PreferenceCategory("Device") {
+ DeviceDetailsContent(metricsState)
+ }
+ }
item {
PreferenceCategory("Details") {
NodeDetailsContent(node)
@@ -176,7 +188,12 @@ private fun NodeDetailList(
}
@Composable
-private fun NodeDetailRow(label: String, icon: ImageVector, value: String) {
+private fun NodeDetailRow(
+ label: String,
+ icon: ImageVector,
+ value: String,
+ iconTint: Color = MaterialTheme.colors.onSurface
+) {
Row(
modifier = Modifier
.fillMaxWidth()
@@ -186,7 +203,8 @@ private fun NodeDetailRow(label: String, icon: ImageVector, value: String) {
Icon(
imageVector = icon,
contentDescription = label,
- modifier = Modifier.size(24.dp)
+ modifier = Modifier.size(24.dp),
+ tint = iconTint
)
Spacer(modifier = Modifier.width(8.dp))
Text(label)
@@ -196,7 +214,49 @@ private fun NodeDetailRow(label: String, icon: ImageVector, value: String) {
}
@Composable
-private fun NodeDetailsContent(node: NodeEntity) {
+private fun DeviceDetailsContent(
+ state: MetricsState,
+) {
+ val node = state.node ?: return
+ val deviceHardware = state.deviceHardware ?: return
+ val hwModelName = deviceHardware.displayName
+ val isSupported = deviceHardware.activelySupported
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .padding(4.dp)
+ .clip(CircleShape)
+ .background(
+ color = Color(node.colors.second).copy(alpha = .5f),
+ shape = CircleShape
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ modifier = Modifier.padding(16.dp),
+ imageVector = ImageVector.vectorResource(deviceHardware.image),
+ contentDescription = hwModelName,
+ )
+ }
+ NodeDetailRow(
+ label = "Hardware",
+ icon = Icons.Default.Router,
+ value = hwModelName
+ )
+ if (isSupported) {
+ NodeDetailRow(
+ label = "Supported",
+ icon = Icons.Default.Verified,
+ value = "",
+ iconTint = Color.Green
+ )
+ }
+}
+
+@Composable
+private fun NodeDetailsContent(
+ node: NodeEntity,
+) {
if (node.mismatchKey) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
@@ -204,18 +264,21 @@ private fun NodeDetailsContent(node: NodeEntity) {
contentDescription = stringResource(id = R.string.encryption_error),
tint = Color.Red,
)
- Column(modifier = Modifier.padding(start = 8.dp)) {
- Text(
- text = stringResource(id = R.string.encryption_error),
- style = MaterialTheme.typography.h6.copy(color = Color.Red)
- )
- Text(
- text = stringResource(id = R.string.encryption_error_text),
- style = MaterialTheme.typography.body2,
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
- )
- }
+ Spacer(Modifier.width(12.dp))
+ Text(
+ text = stringResource(id = R.string.encryption_error),
+ style = MaterialTheme.typography.h6.copy(color = Color.Red),
+ textAlign = TextAlign.Center,
+ )
}
+ Spacer(Modifier.height(16.dp))
+ Text(
+ text = stringResource(id = R.string.encryption_error_text),
+ style = MaterialTheme.typography.body2,
+ color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium),
+ textAlign = TextAlign.Center,
+ )
+ Spacer(Modifier.height(16.dp))
}
NodeDetailRow(
label = "Node Number",
@@ -232,11 +295,6 @@ private fun NodeDetailsContent(node: NodeEntity) {
icon = Icons.Default.Work,
value = node.user.role.name
)
- NodeDetailRow(
- label = "Hardware",
- icon = Icons.Default.Router,
- value = node.user.hwModel.name
- )
if (node.deviceMetrics.uptimeSeconds > 0) {
NodeDetailRow(
label = "Uptime",
@@ -449,6 +507,13 @@ private fun EnvironmentMetrics(
value = "%.2f kg".format(weight)
)
}
+ if (radiation != 0f) {
+ InfoCard(
+ icon = ImageVector.vectorResource(R.drawable.ic_filled_radioactive_24),
+ text = "Radiation",
+ value = "%.1f µR".format(radiation)
+ )
+ }
}
}
@@ -534,6 +599,9 @@ private fun NodeDetailsPreview(
node: NodeEntity
) {
AppTheme {
- NodeDetailList(node, MetricsState.Empty)
+ NodeDetailList(
+ node = node,
+ metricsState = MetricsState.Empty,
+ )
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt
index 4fdc64d7d..6e9a0939b 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt
@@ -60,7 +60,7 @@ import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity
-import com.geeksville.mesh.ui.components.MenuItemAction
+import com.geeksville.mesh.ui.components.NodeMenuAction
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
import com.geeksville.mesh.ui.components.NodeMenu
import com.geeksville.mesh.ui.components.SignalInfo
@@ -79,13 +79,12 @@ fun NodeItem(
gpsFormat: Int,
distanceUnits: Int,
tempInFahrenheit: Boolean,
- ignoreIncomingList: List = emptyList(),
- menuItemActionClicked: (MenuItemAction) -> Unit = {},
+ onAction: (NodeMenuAction) -> Unit = {},
expanded: Boolean = false,
currentTimeMillis: Long,
isConnected: Boolean = false,
) {
- val isIgnored = ignoreIncomingList.contains(thatNode.num)
+ val isIgnored = thatNode.isIgnored
val longName = thatNode.user.longName.ifEmpty { stringResource(id = R.string.unknown_username) }
val isThisNode = thisNode?.num == thatNode.num
@@ -159,17 +158,16 @@ fun NodeItem(
}
NodeMenu(
node = thatNode,
- ignoreIncomingList = ignoreIncomingList,
- isThisNode = isThisNode,
- onMenuItemAction = menuItemActionClicked,
+ showFullMenu = !isThisNode && isConnected,
+ onAction = onAction,
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false },
- isConnected = isConnected,
)
}
NodeKeyStatusIcon(
hasPKC = thatNode.hasPKC,
mismatchKey = thatNode.mismatchKey,
+ publicKey = thatNode.user.publicKey,
modifier = Modifier.size(32.dp)
)
Text(
diff --git a/app/src/main/java/com/geeksville/mesh/ui/ShareFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ShareFragment.kt
index 999db49f4..a41fd7163 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/ShareFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/ShareFragment.kt
@@ -1,3 +1,20 @@
+/*
+ * Copyright (c) 2024 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
package com.geeksville.mesh.ui
import android.os.Bundle
diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
index de2290a8c..a7885aa4a 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
@@ -41,7 +41,7 @@ import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.model.UIViewModel
-import com.geeksville.mesh.ui.components.MenuItemAction
+import com.geeksville.mesh.ui.components.NodeMenuAction
import com.geeksville.mesh.ui.components.NodeFilterTextField
import com.geeksville.mesh.ui.components.rememberTimeTickWithLifecycle
import com.geeksville.mesh.ui.message.navigateToMessages
@@ -131,16 +131,15 @@ fun NodesScreen(
gpsFormat = state.gpsFormat,
distanceUnits = state.distanceUnits,
tempInFahrenheit = state.tempInFahrenheit,
- ignoreIncomingList = state.ignoreIncomingList,
- menuItemActionClicked = { menuItem ->
+ onAction = { menuItem ->
when (menuItem) {
- MenuItemAction.Remove -> model.removeNode(node.num)
- MenuItemAction.Ignore -> model.ignoreNode(node.num)
- MenuItemAction.DirectMessage -> navigateToMessages(node)
- MenuItemAction.RequestUserInfo -> model.requestUserInfo(node.num)
- MenuItemAction.RequestPosition -> model.requestPosition(node.num)
- MenuItemAction.TraceRoute -> model.requestTraceroute(node.num)
- MenuItemAction.MoreDetails -> navigateToNodeDetails(node.num)
+ is NodeMenuAction.Remove -> model.removeNode(node.num)
+ is NodeMenuAction.Ignore -> model.ignoreNode(node)
+ is NodeMenuAction.DirectMessage -> navigateToMessages(node)
+ is NodeMenuAction.RequestUserInfo -> model.requestUserInfo(node.num)
+ is NodeMenuAction.RequestPosition -> model.requestPosition(node.num)
+ is NodeMenuAction.TraceRoute -> model.requestTraceroute(node.num)
+ is NodeMenuAction.MoreDetails -> navigateToNodeDetails(node.num)
}
},
expanded = state.showDetails,
diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/BottomSheetDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/BottomSheetDialog.kt
index 86aded6c1..5833c9870 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/components/BottomSheetDialog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/components/BottomSheetDialog.kt
@@ -1,3 +1,20 @@
+/*
+ * Copyright (c) 2024 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.background
diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/MetricsTimeSelector.kt b/app/src/main/java/com/geeksville/mesh/ui/components/MetricsTimeSelector.kt
index 39ae19b07..117c2d6df 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/components/MetricsTimeSelector.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/components/MetricsTimeSelector.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 8874-9126 Meshtastic LLC
+ * Copyright (c) 2024 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/NodeKeyStatusIcon.kt b/app/src/main/java/com/geeksville/mesh/ui/components/NodeKeyStatusIcon.kt
index 233bbc777..10b814aa2 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/components/NodeKeyStatusIcon.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/components/NodeKeyStatusIcon.kt
@@ -17,8 +17,25 @@
package com.geeksville.mesh.ui.components
+import android.util.Base64
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyOff
import androidx.compose.material.icons.filled.Lock
@@ -27,17 +44,83 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
-import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
import com.geeksville.mesh.R
+import com.geeksville.mesh.model.Channel
+import com.geeksville.mesh.ui.theme.AppTheme
+import com.google.protobuf.ByteString
+
+@Composable
+private fun KeyStatusDialog(
+ @StringRes title: Int,
+ @StringRes text: Int,
+ key: ByteString?,
+ onDismiss: () -> Unit = {}
+) = Dialog(
+ onDismissRequest = onDismiss,
+) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(16.dp),
+ color = MaterialTheme.colors.background
+ ) {
+ LazyColumn(
+ contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ item {
+ Text(
+ text = stringResource(id = title),
+ color = MaterialTheme.colors.onBackground.copy(alpha = ContentAlpha.high),
+ textAlign = TextAlign.Center,
+ )
+ Spacer(Modifier.height(16.dp))
+ Text(
+ text = stringResource(id = text),
+ color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium),
+ textAlign = TextAlign.Center,
+ )
+ Spacer(Modifier.height(16.dp))
+ if (key != null && title == R.string.encryption_pkc) {
+ val keyString = Base64.encodeToString(key.toByteArray(), Base64.NO_WRAP)
+ SelectionContainer {
+ Text(
+ text = "Public Key: $keyString",
+ textAlign = TextAlign.Center,
+ )
+ }
+ Spacer(Modifier.height(16.dp))
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End,
+ ) {
+ TextButton(
+ onClick = onDismiss,
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = MaterialTheme.colors.onSurface,
+ ),
+ ) { Text(text = stringResource(id = R.string.close)) }
+ }
+ }
+ }
+ }
+}
@Composable
fun NodeKeyStatusIcon(
hasPKC: Boolean,
mismatchKey: Boolean,
+ publicKey: ByteString? = null,
modifier: Modifier = Modifier,
) {
var showEncryptionDialog by remember { mutableStateOf(false) }
@@ -47,13 +130,13 @@ fun NodeKeyStatusIcon(
hasPKC -> R.string.encryption_pkc to R.string.encryption_pkc_text
else -> R.string.encryption_psk to R.string.encryption_psk_text
}
- SimpleAlertDialog(title, text) { showEncryptionDialog = false }
+ KeyStatusDialog(title, text, publicKey) { showEncryptionDialog = false }
}
val (icon, tint) = when {
- mismatchKey -> rememberVectorPainter(Icons.Default.KeyOff) to Color.Red
- hasPKC -> rememberVectorPainter(Icons.Default.Lock) to Color(color = 0xFF30C047)
- else -> painterResource(R.drawable.ic_lock_open_right_24) to Color(color = 0xFFFEC30A)
+ mismatchKey -> Icons.Default.KeyOff to Color.Red
+ hasPKC -> Icons.Default.Lock to Color(color = 0xFF30C047)
+ else -> ImageVector.vectorResource(R.drawable.ic_lock_open_right_24) to Color(color = 0xFFFEC30A)
}
IconButton(
@@ -61,7 +144,7 @@ fun NodeKeyStatusIcon(
modifier = modifier,
) {
Icon(
- painter = icon,
+ imageVector = icon,
contentDescription = stringResource(
id = when {
mismatchKey -> R.string.encryption_error
@@ -73,3 +156,39 @@ fun NodeKeyStatusIcon(
)
}
}
+
+@PreviewLightDark
+@Composable
+private fun KeyStatusDialogErrorPreview() {
+ AppTheme {
+ KeyStatusDialog(
+ title = R.string.encryption_error,
+ text = R.string.encryption_error_text,
+ key = null,
+ )
+ }
+}
+
+@PreviewLightDark
+@Composable
+private fun KeyStatusDialogPkcPreview() {
+ AppTheme {
+ KeyStatusDialog(
+ title = R.string.encryption_pkc,
+ text = R.string.encryption_pkc_text,
+ key = Channel.getRandomKey(),
+ )
+ }
+}
+
+@PreviewLightDark
+@Composable
+private fun KeyStatusDialogPskPreview() {
+ AppTheme {
+ KeyStatusDialog(
+ title = R.string.encryption_psk,
+ text = R.string.encryption_psk_text,
+ key = null,
+ )
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/NodeMenu.kt b/app/src/main/java/com/geeksville/mesh/ui/components/NodeMenu.kt
index 9abae0178..bb319e762 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/components/NodeMenu.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/components/NodeMenu.kt
@@ -42,26 +42,23 @@ import com.geeksville.mesh.database.entity.NodeEntity
@Composable
fun NodeMenu(
node: NodeEntity,
- ignoreIncomingList: List,
- isThisNode: Boolean = false,
- onMenuItemAction: (MenuItemAction) -> Unit,
+ showFullMenu: Boolean = false,
onDismissRequest: () -> Unit,
expanded: Boolean = false,
- isConnected: Boolean = false,
+ onAction: (NodeMenuAction) -> Unit
) {
- val isIgnored = ignoreIncomingList.contains(node.num)
var displayIgnoreDialog by remember { mutableStateOf(false) }
var displayRemoveDialog by remember { mutableStateOf(false) }
if (displayIgnoreDialog) {
SimpleAlertDialog(
title = R.string.ignore,
text = stringResource(
- id = if (isIgnored) R.string.ignore_remove else R.string.ignore_add,
+ id = if (node.isIgnored) R.string.ignore_remove else R.string.ignore_add,
node.user.longName
),
onConfirm = {
displayIgnoreDialog = false
- onMenuItemAction(MenuItemAction.Ignore)
+ onAction(NodeMenuAction.Ignore(node))
},
onDismiss = {
displayIgnoreDialog = false
@@ -74,7 +71,7 @@ fun NodeMenu(
text = R.string.remove_node_text,
onConfirm = {
displayRemoveDialog = false
- onMenuItemAction(MenuItemAction.Remove)
+ onAction(NodeMenuAction.Remove(node))
},
onDismiss = {
displayRemoveDialog = false
@@ -87,32 +84,32 @@ fun NodeMenu(
onDismissRequest = onDismissRequest,
) {
- if (!isThisNode && isConnected) {
+ if (showFullMenu) {
DropdownMenuItem(
onClick = {
onDismissRequest()
- onMenuItemAction(MenuItemAction.DirectMessage)
+ onAction(NodeMenuAction.DirectMessage(node))
},
content = { Text(stringResource(R.string.direct_message)) }
)
DropdownMenuItem(
onClick = {
onDismissRequest()
- onMenuItemAction(MenuItemAction.RequestUserInfo)
+ onAction(NodeMenuAction.RequestUserInfo(node))
},
content = { Text(stringResource(R.string.request_userinfo)) }
)
DropdownMenuItem(
onClick = {
onDismissRequest()
- onMenuItemAction(MenuItemAction.RequestPosition)
+ onAction(NodeMenuAction.RequestPosition(node))
},
content = { Text(stringResource(R.string.request_position)) }
)
DropdownMenuItem(
onClick = {
onDismissRequest()
- onMenuItemAction(MenuItemAction.TraceRoute)
+ onAction(NodeMenuAction.TraceRoute(node))
},
content = { Text(stringResource(R.string.traceroute)) }
)
@@ -121,17 +118,15 @@ fun NodeMenu(
onDismissRequest()
displayIgnoreDialog = true
},
- enabled = ignoreIncomingList.size < 3 || isIgnored
) {
Text(stringResource(R.string.ignore))
Spacer(Modifier.weight(1f))
Checkbox(
- checked = isIgnored,
+ checked = node.isIgnored,
onCheckedChange = {
onDismissRequest()
displayIgnoreDialog = true
},
- enabled = isIgnored || ignoreIncomingList.size < 3,
modifier = Modifier.size(24.dp),
)
}
@@ -140,25 +135,26 @@ fun NodeMenu(
onDismissRequest()
displayRemoveDialog = true
},
+ enabled = !node.isIgnored,
) { Text(stringResource(R.string.remove)) }
Divider(Modifier.padding(vertical = 8.dp))
}
DropdownMenuItem(
onClick = {
onDismissRequest()
- onMenuItemAction(MenuItemAction.MoreDetails)
+ onAction(NodeMenuAction.MoreDetails(node))
},
content = { Text(stringResource(R.string.more_details)) }
)
}
}
-enum class MenuItemAction {
- Remove,
- Ignore,
- DirectMessage,
- RequestUserInfo,
- RequestPosition,
- TraceRoute,
- MoreDetails
+sealed class NodeMenuAction {
+ data class Remove(val node: NodeEntity) : NodeMenuAction()
+ data class Ignore(val node: NodeEntity) : NodeMenuAction()
+ data class DirectMessage(val node: NodeEntity) : NodeMenuAction()
+ data class RequestUserInfo(val node: NodeEntity) : NodeMenuAction()
+ data class RequestPosition(val node: NodeEntity) : NodeMenuAction()
+ data class TraceRoute(val node: NodeEntity) : NodeMenuAction()
+ data class MoreDetails(val node: NodeEntity) : NodeMenuAction()
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt b/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt
index 1a5103e55..9470803fb 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt
@@ -67,7 +67,7 @@ import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalClipboardManager
-import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.pluralStringResource
@@ -395,7 +395,7 @@ private fun TextInput(
maxSize: Int = 200,
onClick: (String) -> Unit = {}
) = Column(modifier) {
- val keyboardController = LocalSoftwareKeyboardController.current
+ val focusManager = LocalFocusManager.current
var isFocused by remember { mutableStateOf(false) }
Row(
@@ -426,7 +426,7 @@ private fun TextInput(
if (message.value.text.isNotEmpty()) {
onClick(message.value.text)
message.value = TextFieldValue("")
- keyboardController?.hide()
+ focusManager.clearFocus()
}
},
modifier = Modifier.size(48.dp),
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt
index 32853f592..997af5a2d 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt
@@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Chip
+import androidx.compose.material.ChipDefaults
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.LocalContentColor
@@ -55,16 +56,19 @@ 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 com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R
+import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.ui.components.AutoLinkText
+import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider
import com.geeksville.mesh.ui.theme.AppTheme
@Suppress("LongMethod")
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@Composable
internal fun MessageItem(
- shortName: String?,
+ node: NodeEntity,
messageText: String?,
messageTime: String,
messageStatus: MessageStatus?,
@@ -81,7 +85,7 @@ internal fun MessageItem(
.background(color = if (selected) Color.Gray else MaterialTheme.colors.background),
verticalAlignment = Alignment.CenterVertically,
) {
- val fromLocal = shortName == null
+ val fromLocal = node.user.id == DataPacket.ID_LOCAL
val messageColor = if (fromLocal) R.color.colorMyMsg else R.color.colorMsg
val (topStart, topEnd) = if (fromLocal) 12.dp to 4.dp else 4.dp to 12.dp
val messageModifier = if (fromLocal) {
@@ -110,15 +114,19 @@ internal fun MessageItem(
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
- if (shortName != null) {
+ if (!fromLocal) {
Chip(
onClick = onChipClick,
modifier = Modifier
.padding(end = 8.dp)
.width(72.dp),
+ colors = ChipDefaults.chipColors(
+ backgroundColor = Color(node.colors.second),
+ contentColor = Color(node.colors.first),
+ ),
) {
Text(
- text = shortName,
+ text = node.user.shortName,
modifier = Modifier.fillMaxWidth(),
fontSize = MaterialTheme.typography.button.fontSize,
fontWeight = FontWeight.Normal,
@@ -129,11 +137,14 @@ internal fun MessageItem(
Column(
modifier = Modifier.padding(top = 8.dp),
) {
-// Text(
-// text = longName ?: stringResource(id = R.string.unknown_username),
-// color = MaterialTheme.colors.onSurface,
-// fontSize = MaterialTheme.typography.button.fontSize,
-// )
+// if (!fromLocal) {
+// Text(
+// text = with(node.user) { "$longName ($id)" },
+// modifier = Modifier.padding(bottom = 4.dp),
+// color = MaterialTheme.colors.onSurface,
+// fontSize = MaterialTheme.typography.caption.fontSize,
+// )
+// }
AutoLinkText(
text = messageText.orEmpty(),
style = LocalTextStyle.current.copy(
@@ -181,8 +192,7 @@ internal fun MessageItem(
private fun MessageItemPreview() {
AppTheme {
MessageItem(
- shortName = stringResource(R.string.some_username),
- // longName = stringResource(R.string.unknown_username),
+ node = NodeEntityPreviewParameterProvider().values.first(),
messageText = stringResource(R.string.sample_message),
messageTime = "10:00",
messageStatus = MessageStatus.DELIVERED,
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageList.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageList.kt
index 42c30588d..3d8ae96a7 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageList.kt
@@ -33,6 +33,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.database.entity.Reaction
import com.geeksville.mesh.model.Message
@@ -50,6 +52,7 @@ internal fun MessageList(
onSendReaction: (String, Int) -> Unit,
onClick: (Message) -> Unit = {}
) {
+ val haptics = LocalHapticFeedback.current
val inSelectionMode by remember { derivedStateOf { selectedIds.value.isNotEmpty() } }
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = messages.indexOfLast { !it.read }.coerceAtLeast(0)
@@ -70,10 +73,10 @@ internal fun MessageList(
ReactionDialog(reactions) { showReactionDialog = null }
}
- fun toggle(uuid: Long) = if (selectedIds.value.contains(uuid)) {
- selectedIds.value -= uuid
+ fun MutableState>.toggle(uuid: Long) = if (value.contains(uuid)) {
+ value -= uuid
} else {
- selectedIds.value += uuid
+ value += uuid
}
LazyColumn(
@@ -83,18 +86,21 @@ internal fun MessageList(
contentPadding = contentPadding
) {
items(messages, key = { it.uuid }) { msg ->
- val fromLocal = msg.user.id == DataPacket.ID_LOCAL
+ val fromLocal = msg.node.user.id == DataPacket.ID_LOCAL
val selected by remember { derivedStateOf { selectedIds.value.contains(msg.uuid) } }
ReactionRow(fromLocal, msg.emojis) { showReactionDialog = msg.emojis }
MessageItem(
- shortName = msg.user.shortName.takeIf { !fromLocal },
+ node = msg.node,
messageText = msg.text,
messageTime = msg.time,
messageStatus = msg.status,
selected = selected,
- onClick = { if (inSelectionMode) toggle(msg.uuid) },
- onLongClick = { toggle(msg.uuid) },
+ onClick = { if (inSelectionMode) selectedIds.toggle(msg.uuid) },
+ onLongClick = {
+ selectedIds.toggle(msg.uuid)
+ haptics.performHapticFeedback(HapticFeedbackType.LongPress)
+ },
onChipClick = { onClick(msg) },
onStatusClick = { showStatusDialog = msg },
onSendReaction = { onSendReaction(it, msg.packetId) },
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.jpg b/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.jpg
new file mode 100644
index 000000000..83442ac05
Binary files /dev/null and b/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.jpg differ
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png b/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png
deleted file mode 100644
index 78a81dccc..000000000
Binary files a/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png and /dev/null differ
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.jpg b/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.jpg
new file mode 100644
index 000000000..9ce58fc3f
Binary files /dev/null and b/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.jpg differ
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png b/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png
deleted file mode 100644
index a884644e0..000000000
Binary files a/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png and /dev/null differ
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.jpg b/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.jpg
new file mode 100644
index 000000000..8e6966e1b
Binary files /dev/null and b/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.jpg differ
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png b/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png
deleted file mode 100644
index c198e8b39..000000000
Binary files a/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png and /dev/null differ
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.jpg b/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.jpg
new file mode 100644
index 000000000..97427e204
Binary files /dev/null and b/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.jpg differ
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png b/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png
deleted file mode 100644
index 63d3f1e20..000000000
Binary files a/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png and /dev/null differ
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.jpg b/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.jpg
new file mode 100644
index 000000000..04340960d
Binary files /dev/null and b/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.jpg differ
diff --git a/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.png b/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.png
deleted file mode 100644
index 1ed4e9df3..000000000
Binary files a/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.png and /dev/null differ
diff --git a/app/src/main/res/drawable/hw_diy.xml b/app/src/main/res/drawable/hw_diy.xml
new file mode 100644
index 000000000..feedd56c7
--- /dev/null
+++ b/app/src/main/res/drawable/hw_diy.xml
@@ -0,0 +1,1631 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_ht62_esp32c3_sx1262.xml b/app/src/main/res/drawable/hw_heltec_ht62_esp32c3_sx1262.xml
new file mode 100644
index 000000000..1a0c19777
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_ht62_esp32c3_sx1262.xml
@@ -0,0 +1,1714 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_mesh_node_t114.xml b/app/src/main/res/drawable/hw_heltec_mesh_node_t114.xml
new file mode 100644
index 000000000..457ffd6cd
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_mesh_node_t114.xml
@@ -0,0 +1,277 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_mesh_node_t114_case.xml b/app/src/main/res/drawable/hw_heltec_mesh_node_t114_case.xml
new file mode 100644
index 000000000..e95e13e6c
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_mesh_node_t114_case.xml
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_v3.xml b/app/src/main/res/drawable/hw_heltec_v3.xml
new file mode 100644
index 000000000..2031cb196
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_v3.xml
@@ -0,0 +1,1141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_v3_case.xml b/app/src/main/res/drawable/hw_heltec_v3_case.xml
new file mode 100644
index 000000000..5e444b6f0
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_v3_case.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_vision_master_e213.xml b/app/src/main/res/drawable/hw_heltec_vision_master_e213.xml
new file mode 100644
index 000000000..017b80bdd
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_vision_master_e213.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_vision_master_e290.xml b/app/src/main/res/drawable/hw_heltec_vision_master_e290.xml
new file mode 100644
index 000000000..f617ecd74
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_vision_master_e290.xml
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_vision_master_t190.xml b/app/src/main/res/drawable/hw_heltec_vision_master_t190.xml
new file mode 100644
index 000000000..6fd688f1d
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_vision_master_t190.xml
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_wireless_paper.xml b/app/src/main/res/drawable/hw_heltec_wireless_paper.xml
new file mode 100644
index 000000000..66db18fc7
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_wireless_paper.xml
@@ -0,0 +1,428 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_wireless_paper_v1_0.xml b/app/src/main/res/drawable/hw_heltec_wireless_paper_v1_0.xml
new file mode 100644
index 000000000..66db18fc7
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_wireless_paper_v1_0.xml
@@ -0,0 +1,428 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_wireless_tracker.xml b/app/src/main/res/drawable/hw_heltec_wireless_tracker.xml
new file mode 100644
index 000000000..74d14932d
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_wireless_tracker.xml
@@ -0,0 +1,1619 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_wireless_tracker_v1_0.xml b/app/src/main/res/drawable/hw_heltec_wireless_tracker_v1_0.xml
new file mode 100644
index 000000000..74d14932d
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_wireless_tracker_v1_0.xml
@@ -0,0 +1,1619 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_heltec_wsl_v3.xml b/app/src/main/res/drawable/hw_heltec_wsl_v3.xml
new file mode 100644
index 000000000..f4246b21b
--- /dev/null
+++ b/app/src/main/res/drawable/hw_heltec_wsl_v3.xml
@@ -0,0 +1,1496 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_nano_g2_ultra.xml b/app/src/main/res/drawable/hw_nano_g2_ultra.xml
new file mode 100644
index 000000000..60bfb06db
--- /dev/null
+++ b/app/src/main/res/drawable/hw_nano_g2_ultra.xml
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_pico.xml b/app/src/main/res/drawable/hw_pico.xml
new file mode 100644
index 000000000..eb3935ab3
--- /dev/null
+++ b/app/src/main/res/drawable/hw_pico.xml
@@ -0,0 +1,1717 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_promicro.xml b/app/src/main/res/drawable/hw_promicro.xml
new file mode 100644
index 000000000..2f69e14ca
--- /dev/null
+++ b/app/src/main/res/drawable/hw_promicro.xml
@@ -0,0 +1,1555 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_rak11310.xml b/app/src/main/res/drawable/hw_rak11310.xml
new file mode 100644
index 000000000..5d39943f8
--- /dev/null
+++ b/app/src/main/res/drawable/hw_rak11310.xml
@@ -0,0 +1,1228 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_rak4631.xml b/app/src/main/res/drawable/hw_rak4631.xml
new file mode 100644
index 000000000..ff6225c06
--- /dev/null
+++ b/app/src/main/res/drawable/hw_rak4631.xml
@@ -0,0 +1,1999 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_rak4631_case.xml b/app/src/main/res/drawable/hw_rak4631_case.xml
new file mode 100644
index 000000000..d937ffbc7
--- /dev/null
+++ b/app/src/main/res/drawable/hw_rak4631_case.xml
@@ -0,0 +1,263 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_rak_wismeshtap.xml b/app/src/main/res/drawable/hw_rak_wismeshtap.xml
new file mode 100644
index 000000000..fb9084b49
--- /dev/null
+++ b/app/src/main/res/drawable/hw_rak_wismeshtap.xml
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_rpipicow.xml b/app/src/main/res/drawable/hw_rpipicow.xml
new file mode 100644
index 000000000..961078561
--- /dev/null
+++ b/app/src/main/res/drawable/hw_rpipicow.xml
@@ -0,0 +1,1645 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_seeed_sensecap_indicator.xml b/app/src/main/res/drawable/hw_seeed_sensecap_indicator.xml
new file mode 100644
index 000000000..9aecd2b97
--- /dev/null
+++ b/app/src/main/res/drawable/hw_seeed_sensecap_indicator.xml
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_seeed_xiao_s3.xml b/app/src/main/res/drawable/hw_seeed_xiao_s3.xml
new file mode 100644
index 000000000..242e2d734
--- /dev/null
+++ b/app/src/main/res/drawable/hw_seeed_xiao_s3.xml
@@ -0,0 +1,711 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_station_g2.xml b/app/src/main/res/drawable/hw_station_g2.xml
new file mode 100644
index 000000000..72f187ae5
--- /dev/null
+++ b/app/src/main/res/drawable/hw_station_g2.xml
@@ -0,0 +1,437 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_t_deck.xml b/app/src/main/res/drawable/hw_t_deck.xml
new file mode 100644
index 000000000..d13a6f1e2
--- /dev/null
+++ b/app/src/main/res/drawable/hw_t_deck.xml
@@ -0,0 +1,658 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_t_echo.xml b/app/src/main/res/drawable/hw_t_echo.xml
new file mode 100644
index 000000000..c9642c1bc
--- /dev/null
+++ b/app/src/main/res/drawable/hw_t_echo.xml
@@ -0,0 +1,247 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_t_watch_s3.xml b/app/src/main/res/drawable/hw_t_watch_s3.xml
new file mode 100644
index 000000000..9da42a48d
--- /dev/null
+++ b/app/src/main/res/drawable/hw_t_watch_s3.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_tbeam.xml b/app/src/main/res/drawable/hw_tbeam.xml
new file mode 100644
index 000000000..1ab61b314
--- /dev/null
+++ b/app/src/main/res/drawable/hw_tbeam.xml
@@ -0,0 +1,2693 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_tbeam_s3_core.xml b/app/src/main/res/drawable/hw_tbeam_s3_core.xml
new file mode 100644
index 000000000..8387e402d
--- /dev/null
+++ b/app/src/main/res/drawable/hw_tbeam_s3_core.xml
@@ -0,0 +1,1583 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_tlora_c6.xml b/app/src/main/res/drawable/hw_tlora_c6.xml
new file mode 100644
index 000000000..dad87ed5c
--- /dev/null
+++ b/app/src/main/res/drawable/hw_tlora_c6.xml
@@ -0,0 +1,515 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_tlora_t3s3_epaper.xml b/app/src/main/res/drawable/hw_tlora_t3s3_epaper.xml
new file mode 100644
index 000000000..37cc0e043
--- /dev/null
+++ b/app/src/main/res/drawable/hw_tlora_t3s3_epaper.xml
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_tlora_t3s3_v1.xml b/app/src/main/res/drawable/hw_tlora_t3s3_v1.xml
new file mode 100644
index 000000000..42e5695ab
--- /dev/null
+++ b/app/src/main/res/drawable/hw_tlora_t3s3_v1.xml
@@ -0,0 +1,932 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_tlora_v2_1_1_6.xml b/app/src/main/res/drawable/hw_tlora_v2_1_1_6.xml
new file mode 100644
index 000000000..123a78383
--- /dev/null
+++ b/app/src/main/res/drawable/hw_tlora_v2_1_1_6.xml
@@ -0,0 +1,880 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_tlora_v2_1_1_8.xml b/app/src/main/res/drawable/hw_tlora_v2_1_1_8.xml
new file mode 100644
index 000000000..123a78383
--- /dev/null
+++ b/app/src/main/res/drawable/hw_tlora_v2_1_1_8.xml
@@ -0,0 +1,880 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_tracker_t1000_e.xml b/app/src/main/res/drawable/hw_tracker_t1000_e.xml
new file mode 100644
index 000000000..2efed4673
--- /dev/null
+++ b/app/src/main/res/drawable/hw_tracker_t1000_e.xml
@@ -0,0 +1,259 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_unknown.xml b/app/src/main/res/drawable/hw_unknown.xml
new file mode 100644
index 000000000..e12d55def
--- /dev/null
+++ b/app/src/main/res/drawable/hw_unknown.xml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_wio_tracker_wm1110.xml b/app/src/main/res/drawable/hw_wio_tracker_wm1110.xml
new file mode 100644
index 000000000..27ac4e7dc
--- /dev/null
+++ b/app/src/main/res/drawable/hw_wio_tracker_wm1110.xml
@@ -0,0 +1,2178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hw_wm1110_dev_kit.xml b/app/src/main/res/drawable/hw_wm1110_dev_kit.xml
new file mode 100644
index 000000000..94c9bdf22
--- /dev/null
+++ b/app/src/main/res/drawable/hw_wm1110_dev_kit.xml
@@ -0,0 +1,3080 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_filled_radioactive_24.xml b/app/src/main/res/drawable/ic_filled_radioactive_24.xml
new file mode 100644
index 000000000..13fdb3021
--- /dev/null
+++ b/app/src/main/res/drawable/ic_filled_radioactive_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
index d761fbff6..f82d8d8e5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -13,7 +13,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.7.2'
+ classpath 'com.android.tools.build:gradle:8.7.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
diff --git a/config/detekt/detekt-baseline.xml b/config/detekt/detekt-baseline.xml
index c41d1c503..a1b8943b3 100644
--- a/config/detekt/detekt-baseline.xml
+++ b/config/detekt/detekt-baseline.xml
@@ -2,6 +2,7 @@
+ AbsentOrWrongFileLicense:LazyColumnDragAndDropDemo.kt$com.geeksville.mesh.ui.components.LazyColumnDragAndDropDemo.kt
ChainWrapping:Channel.kt$Channel$&&
ChainWrapping:CustomTileSource.kt$CustomTileSource.Companion.<no name provided>$+
ChainWrapping:SqlTileWriterExt.kt$SqlTileWriterExt$+
diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml
index eb528e777..b27dfe37e 100644
--- a/config/detekt/detekt.yml
+++ b/config/detekt/detekt.yml
@@ -53,7 +53,7 @@ output-reports:
comments:
active: true
AbsentOrWrongFileLicense:
- active: false
+ active: true
licenseTemplateFile: 'license.template'
licenseTemplateIsRegex: false
CommentOverPrivateFunction:
diff --git a/config/detekt/license.template b/config/detekt/license.template
new file mode 100644
index 000000000..63c871a20
--- /dev/null
+++ b/config/detekt/license.template
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2024 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+