feat: network module (#1905)

This commit is contained in:
James Rich
2025-05-22 08:30:08 -05:00
committed by GitHub
parent 520d058546
commit 02bb3f02e4
80 changed files with 2165 additions and 15032 deletions

View File

@@ -0,0 +1,75 @@
name: Update Firmware Releases List
on:
schedule:
- cron: '0 * * * *' # Run every hour
workflow_dispatch: # Allow manual triggering
jobs:
update-hardware-list:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Fetch latest firmware releases data
id: fetch-data
run: |
# Define variables for file paths
firmware_releases_json="app/src/main/assets/firmware_releases.json"
new_firmware_releases_json="/tmp/new_firmware_releases.json"
# Fetch data from API
curl -s --fail https://api.meshtastic.org/github/firmware/list > "$new_firmware_releases_json"
# Ensure the output is valid JSON
if ! jq empty "$new_firmware_releases_json" 2>/dev/null; then
echo "::error::API returned invalid JSON data"
exit 1
fi
# Check if "$firmware_releases_json" exists
if [ -f "$firmware_releases_json" ]; then
# Format both files for consistent comparison
jq --sort-keys . "$new_firmware_releases_json" > /tmp/new-formatted.json
jq --sort-keys . "$firmware_releases_json" > /tmp/existing-formatted.json
# Compare files
if cmp -s /tmp/new-formatted.json /tmp/existing-formatted.json; then
echo "No changes detected in hardware list"
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "Changes detected in hardware list"
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
else
echo "firmware_releases.json doesn't exist yet"
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
# Copy new data to destination
cp "$new_firmware_releases_json" "$firmware_releases_json"
- name: Create Pull Request
if: steps.fetch-data.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: update firmware releases list from Meshtastic API"
title: "chore: update firmware releases list from Meshtastic API"
body: |
This PR updates the firmware releases list with the latest data from the Meshtastic API.
This PR was automatically generated by the update-hardware-list workflow.
branch: update-hardware-list
base: master
delete-branch: true

View File

@@ -144,7 +144,7 @@ androidComponents {
}
dependencies {
implementation project(":network")
implementation(fileTree(dir: 'libs', include: ['*.jar']))
// Bundles
@@ -159,6 +159,7 @@ dependencies {
implementation(libs.bundles.hilt)
implementation(libs.bundles.protobuf)
implementation(libs.bundles.coil)
//OSM
implementation(libs.bundles.osm)
implementation(libs.osmdroid.geopackage){ exclude group: "com.j256.ormlite" }

View File

@@ -0,0 +1,695 @@
{
"formatVersion": 1,
"database": {
"version": 17,
"identityHash": "6f25f17fe4f83769489c264d4dae0398",
"entities": [
{
"tableName": "my_node",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `deviceId` TEXT, PRIMARY KEY(`myNodeNum`))",
"fields": [
{
"fieldPath": "myNodeNum",
"columnName": "myNodeNum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "model",
"columnName": "model",
"affinity": "TEXT"
},
{
"fieldPath": "firmwareVersion",
"columnName": "firmwareVersion",
"affinity": "TEXT"
},
{
"fieldPath": "couldUpdate",
"columnName": "couldUpdate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shouldUpdate",
"columnName": "shouldUpdate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "currentPacketId",
"columnName": "currentPacketId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageTimeoutMsec",
"columnName": "messageTimeoutMsec",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minAppVersion",
"columnName": "minAppVersion",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "maxChannels",
"columnName": "maxChannels",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasWifi",
"columnName": "hasWifi",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "deviceId",
"columnName": "deviceId",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"myNodeNum"
]
}
},
{
"tableName": "nodes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `user` BLOB NOT NULL, `long_name` TEXT, `short_name` TEXT, `position` BLOB NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `last_heard` INTEGER NOT NULL, `device_metrics` BLOB NOT NULL, `channel` INTEGER NOT NULL, `via_mqtt` INTEGER NOT NULL, `hops_away` INTEGER NOT NULL, `is_favorite` INTEGER NOT NULL, `is_ignored` INTEGER NOT NULL DEFAULT 0, `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"
},
{
"fieldPath": "shortName",
"columnName": "short_name",
"affinity": "TEXT"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "latitude",
"columnName": "latitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "longitude",
"columnName": "longitude",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "snr",
"columnName": "snr",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "rssi",
"columnName": "rssi",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastHeard",
"columnName": "last_heard",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "deviceTelemetry",
"columnName": "device_metrics",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "channel",
"columnName": "channel",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "viaMqtt",
"columnName": "via_mqtt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hopsAway",
"columnName": "hops_away",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isFavorite",
"columnName": "is_favorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isIgnored",
"columnName": "is_ignored",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "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"
]
}
},
{
"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`)"
}
]
},
{
"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"
]
}
},
{
"tableName": "log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, `from_num` INTEGER NOT NULL DEFAULT 0, `port_num` INTEGER NOT NULL DEFAULT 0, `from_radio` BLOB NOT NULL DEFAULT x'', PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message_type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "received_date",
"columnName": "received_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "raw_message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fromNum",
"columnName": "from_num",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "portNum",
"columnName": "port_num",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "fromRadio",
"columnName": "from_radio",
"affinity": "BLOB",
"notNull": true,
"defaultValue": "x''"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uuid"
]
},
"indices": [
{
"name": "index_log_from_num",
"unique": false,
"columnNames": [
"from_num"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_log_from_num` ON `${TABLE_NAME}` (`from_num`)"
},
{
"name": "index_log_port_num",
"unique": false,
"columnNames": [
"port_num"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_log_port_num` ON `${TABLE_NAME}` (`port_num`)"
}
]
},
{
"tableName": "quick_chat",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uuid"
]
}
},
{
"tableName": "reactions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`)"
}
]
},
{
"tableName": "metadata",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `proto` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`num`))",
"fields": [
{
"fieldPath": "num",
"columnName": "num",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "proto",
"columnName": "proto",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"num"
]
},
"indices": [
{
"name": "index_metadata_num",
"unique": false,
"columnNames": [
"num"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_metadata_num` ON `${TABLE_NAME}` (`num`)"
}
]
},
{
"tableName": "device_hardware",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`actively_supported` INTEGER NOT NULL, `architecture` TEXT NOT NULL, `display_name` TEXT NOT NULL, `has_ink_hud` INTEGER, `has_mui` INTEGER, `hwModel` INTEGER NOT NULL, `hw_model_slug` TEXT NOT NULL, `images` TEXT, `last_updated` INTEGER NOT NULL, `partition_scheme` TEXT, `platformio_target` TEXT NOT NULL, `requires_dfu` INTEGER, `support_level` INTEGER, `tags` TEXT, PRIMARY KEY(`hwModel`))",
"fields": [
{
"fieldPath": "activelySupported",
"columnName": "actively_supported",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "architecture",
"columnName": "architecture",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "display_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasInkHud",
"columnName": "has_ink_hud",
"affinity": "INTEGER"
},
{
"fieldPath": "hasMui",
"columnName": "has_mui",
"affinity": "INTEGER"
},
{
"fieldPath": "hwModel",
"columnName": "hwModel",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hwModelSlug",
"columnName": "hw_model_slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "images",
"columnName": "images",
"affinity": "TEXT"
},
{
"fieldPath": "lastUpdated",
"columnName": "last_updated",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "partitionScheme",
"columnName": "partition_scheme",
"affinity": "TEXT"
},
{
"fieldPath": "platformioTarget",
"columnName": "platformio_target",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "requiresDfu",
"columnName": "requires_dfu",
"affinity": "INTEGER"
},
{
"fieldPath": "supportLevel",
"columnName": "support_level",
"affinity": "INTEGER"
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"hwModel"
]
}
},
{
"tableName": "firmware_release",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `page_url` TEXT NOT NULL, `release_notes` TEXT NOT NULL, `title` TEXT NOT NULL, `zip_url` TEXT NOT NULL, `last_updated` INTEGER NOT NULL, `release_type` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pageUrl",
"columnName": "page_url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "releaseNotes",
"columnName": "release_notes",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "zipUrl",
"columnName": "zip_url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastUpdated",
"columnName": "last_updated",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "releaseType",
"columnName": "release_type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"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, '6f25f17fe4f83769489c264d4dae0398')"
]
}
}

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 89 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="795.27 277.13 409.46 1319.35"><defs><style>.cls-1{fill:#353535;}.cls-2{fill:#1e1e1d;}.cls-3{fill:#b1a368;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8,.cls-9{fill:none;}.cls-4,.cls-6{stroke:#050606;}.cls-10,.cls-11,.cls-4,.cls-6,.cls-8{stroke-miterlimit:10;}.cls-4{stroke-width:2.41px;}.cls-5{fill:#30c2db;}.cls-6{stroke-width:3.91px;}.cls-7{fill:#dcf0f2;}.cls-10,.cls-11,.cls-8{stroke:#dcf0f2;}.cls-8{stroke-width:1.81px;}.cls-9{stroke:#17afbf;stroke-linecap:round;stroke-linejoin:round;stroke-width:7.23px;}.cls-10{stroke-width:1.78px;}.cls-11{stroke-width:1.81px;}</style></defs><g id="Layer_7" data-name="Layer 7"><path class="cls-1" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-2" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-2" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-3" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-3" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-3" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-3" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-3" x="885.8" y="715.74" width="84.14" height="7.28"></rect><rect class="cls-4" x="885.8" y="844.3" width="84.14" height="19.02"></rect><rect class="cls-4" x="880.6" y="819.07" width="92.65" height="25.23"></rect><rect class="cls-4" x="885.8" y="790.65" width="84.14" height="28.41"></rect><rect class="cls-4" x="880.6" y="723.02" width="92.65" height="67.63"></rect><rect class="cls-4" x="885.8" y="715.74" width="84.14" height="7.28"></rect><path class="cls-4" d="M915.62,278.34h22.61a35,35,0,0,1,35,35V715.74a0,0,0,0,1,0,0H880.6a0,0,0,0,1,0,0V313.36A35,35,0,0,1,915.62,278.34Z"></path><rect class="cls-4" x="880.6" y="340.15" width="92.65" height="7.54"></rect><rect class="cls-4" x="880.6" y="356.68" width="92.65" height="7.54"></rect><rect class="cls-5" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><rect class="cls-1" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-6" x="900.05" y="973.19" width="202.03" height="354.65" rx="16.4"></rect><rect class="cls-7" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-7" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" x="871.51" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><rect class="cls-4" x="1070.16" y="890.41" width="55.42" height="31.12" rx="15.56"></rect><circle class="cls-8" cx="841.7" cy="1537.01" r="16.25"></circle><circle class="cls-8" cx="841.7" cy="913.26" r="16.25"></circle><circle class="cls-8" cx="1157.32" cy="913.26" r="16.25"></circle><circle class="cls-8" cx="1157.32" cy="1504.51" r="16.25"></circle><line class="cls-9" x1="942.51" y1="1592.42" x2="942.51" y2="1381.55"></line><line class="cls-9" x1="966.52" y1="1592.42" x2="966.52" y2="1381.55"></line><line class="cls-9" x1="990.57" y1="1592.42" x2="990.57" y2="1381.55"></line><line class="cls-9" x1="1014.59" y1="1592.42" x2="1014.59" y2="1381.55"></line><line class="cls-9" x1="1038.63" y1="1592.42" x2="1038.63" y2="1381.55"></line><line class="cls-9" x1="1062.65" y1="1592.42" x2="1062.65" y2="1381.55"></line><rect class="cls-4" x="796.48" y="856.3" width="407.05" height="738.98" rx="47.74"></rect><path class="cls-10" d="M1040.1,947.74H960.65A13.93,13.93,0,0,1,947,936.64l-10.23-49.2a13.93,13.93,0,0,1,13.64-16.77h97.72a13.93,13.93,0,0,1,13.75,16.18l-8,49.2A13.94,13.94,0,0,1,1040.1,947.74Z"></path><rect class="cls-11" x="816.35" y="870.67" width="365.51" height="703.12" rx="32.37"></rect><rect class="cls-11" x="888.77" y="963.84" width="223.2" height="374.66" rx="25.21"></rect></g></svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="404.68 390.65 1217.15 959.26"><defs><style>.cls-1{fill:#dfeaf7;}.cls-2{fill:#17907f;}.cls-3{fill:#2b2b2b;}.cls-4,.cls-5{fill:none;stroke:#050606;stroke-miterlimit:10;}.cls-4{stroke-width:2.25px;}.cls-5{stroke-width:4px;}.cls-6{fill:#050606;}</style></defs><g id="Layer_5" data-name="Layer 5"><path class="cls-1" d="M1517.73,392.65h0a102.1,102.1,0,0,0-102.1,102.1V770.37A40.62,40.62,0,0,1,1375,811H455.16a48.49,48.49,0,0,0-48.48,48.48v126a11.85,11.85,0,0,0,3.46,8.37l15.34,15.34a11.81,11.81,0,0,1,3.47,8.37v137.16a11.81,11.81,0,0,1-3.47,8.37l-15.34,15.34a11.85,11.85,0,0,0-3.46,8.37v112.67a48.49,48.49,0,0,0,48.48,48.49H1571.34a48.51,48.51,0,0,0,48.49-48.5V494.75A102.1,102.1,0,0,0,1517.73,392.65Zm-110.61,815V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><path class="cls-2" d="M1516,439.16c-30.23.91-53.92,26.54-53.92,56.79V770.37A87.11,87.11,0,0,1,1375,857.48H732.31A27.51,27.51,0,0,0,704.8,885v388.93a27.51,27.51,0,0,0,27.51,27.51h828.38a12.7,12.7,0,0,0,12.65-12.65v-794A55.69,55.69,0,0,0,1516,439.16Zm-108.9,768.47V954a33.14,33.14,0,0,1,66.27,0v253.65a33.14,33.14,0,0,1-66.27,0Z"></path><rect class="cls-3" x="787.14" y="943.38" width="429.45" height="224.42"></rect><path class="cls-1" d="M1478.6,915.35A54.23,54.23,0,0,0,1386,953.69v254.23a54.23,54.23,0,1,0,108.45,0V953.69A54,54,0,0,0,1478.6,915.35Zm-5.21,292.28a33.14,33.14,0,0,1-66.27,0V954a33.14,33.14,0,0,1,66.27,0Z"></path></g><g id="Layer_2" data-name="Layer 2"><path class="cls-4" d="M1573.34,494.75v794a12.68,12.68,0,0,1-12.65,12.65H732.31a27.51,27.51,0,0,1-27.51-27.51V885a27.51,27.51,0,0,1,27.51-27.51H1375a87.11,87.11,0,0,0,87.11-87.11V496c0-30.25,23.69-55.88,53.92-56.79A55.69,55.69,0,0,1,1573.34,494.75Z"></path><path class="cls-5" d="M410.14,1178.39,425.49,1163a11.78,11.78,0,0,0,3.46-8.35V1017.5a11.8,11.8,0,0,0-3.46-8.35l-15.35-15.36a11.77,11.77,0,0,1-3.46-8.35v-126A48.47,48.47,0,0,1,455.16,811H1375a40.63,40.63,0,0,0,40.63-40.63V494.75a102.1,102.1,0,0,1,102.1-102.1h0a102.1,102.1,0,0,1,102.1,102.1v804.66a48.51,48.51,0,0,1-48.49,48.5H455.16a48.48,48.48,0,0,1-48.48-48.49V1186.74A11.78,11.78,0,0,1,410.14,1178.39Z"></path><rect class="cls-4" x="1407.12" y="920.85" width="66.26" height="319.9" rx="33.13"></rect><rect class="cls-4" x="1386.03" y="899.46" width="108.46" height="362.69" rx="54.23"></rect><path class="cls-6" d="M639.76,1070.55a2.91,2.91,0,0,1-2.91-2.91v-30.53a5.42,5.42,0,0,0-1.6-3.86l-32.44-32.44a11.86,11.86,0,0,1-3.5-8.44V901a12.52,12.52,0,0,0-12.51-12.51H483.92a12.7,12.7,0,0,0-12.68,12.69v76.78a24.13,24.13,0,0,0,7.11,17.18l14.33,14.33a24.13,24.13,0,0,0,17.18,7.11h50.75a11.86,11.86,0,0,1,8.44,3.5l24.26,24.26a12.47,12.47,0,0,1,3.68,8.88v14.46a2.91,2.91,0,1,1-5.81,0v-14.46a6.72,6.72,0,0,0-2-4.77l-24.26-24.26a6.09,6.09,0,0,0-4.33-1.8H509.86a29.91,29.91,0,0,1-21.29-8.81l-14.33-14.33a29.87,29.87,0,0,1-8.81-21.29V901.14a18.51,18.51,0,0,1,18.49-18.5H586.8A18.34,18.34,0,0,1,605.12,901v91.41a6.09,6.09,0,0,0,1.8,4.33l32.44,32.44a11.19,11.19,0,0,1,3.3,8v30.53A2.9,2.9,0,0,1,639.76,1070.55Z"></path><path class="cls-6" d="M586.8,1289.46H483.92a18.51,18.51,0,0,1-18.49-18.5v-76.78a29.87,29.87,0,0,1,8.81-21.29l14.33-14.34a29.91,29.91,0,0,1,21.29-8.81h50.75a6.09,6.09,0,0,0,4.33-1.8l24.26-24.25a6.74,6.74,0,0,0,2-4.78v-14.46a2.91,2.91,0,0,1,5.81,0v14.46a12.51,12.51,0,0,1-3.68,8.89l-24.26,24.25a11.86,11.86,0,0,1-8.44,3.5H509.86a24.17,24.17,0,0,0-17.18,7.11L478.35,1177a24.09,24.09,0,0,0-7.11,17.18V1271a12.71,12.71,0,0,0,12.68,12.69H586.8a12.53,12.53,0,0,0,12.51-12.52v-91.4a11.83,11.83,0,0,1,3.5-8.44l32.44-32.44a5.46,5.46,0,0,0,1.6-3.87v-30.53a2.91,2.91,0,0,1,5.81,0V1135a11.19,11.19,0,0,1-3.3,8l-32.44,32.45a6.06,6.06,0,0,0-1.8,4.33v91.4A18.35,18.35,0,0,1,586.8,1289.46Z"></path><rect class="cls-4" x="787.14" y="943.38" width="429.45" height="224.42"></rect></g></svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="528.89 806.04 942.22 446.84"><defs><style>.cls-1{fill:#e8eae8;}.cls-2{fill:#dbdddb;}.cls-3{fill:#c6842a;}.cls-4,.cls-5,.cls-7,.cls-9{fill:none;stroke-miterlimit:10;}.cls-4,.cls-5{stroke:#050606;}.cls-4,.cls-9{stroke-width:2.44px;}.cls-5,.cls-7{stroke-width:1.22px;}.cls-6{fill:#b7b7b7;}.cls-7,.cls-9{stroke:#b7b7b7;}.cls-8{fill:#cbcccb;}.cls-10{fill:#434543;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M1469.9,826.68v390.94a19.43,19.43,0,0,1-19.43,19.43H549.53a19.43,19.43,0,0,1-19.43-19.43V826.68a19.42,19.42,0,0,1,19.43-19.42h900.94A19.42,19.42,0,0,1,1469.9,826.68Z"></path><path class="cls-2" d="M574.23,807.26v429.79h-24.7a19.43,19.43,0,0,1-19.43-19.43V826.68a19.42,19.42,0,0,1,19.43-19.42Z"></path><path class="cls-2" d="M1469.9,826.68v390.94a19.43,19.43,0,0,1-19.43,19.43h-37.56V807.26h37.56A19.42,19.42,0,0,1,1469.9,826.68Z"></path><path class="cls-3" d="M574.23,1129.8h-7.47a4.55,4.55,0,0,1-4.55-4.54V919.05a4.55,4.55,0,0,1,4.55-4.55h7.47"></path><rect class="cls-4" x="530.11" y="807.26" width="939.78" height="429.79" rx="19.42"></rect><line class="cls-5" x1="574.23" y1="807.26" x2="574.23" y2="1237.05"></line><path class="cls-5" d="M574.23,1129.8h-7.47a4.55,4.55,0,0,1-4.55-4.54V919.05a4.55,4.55,0,0,1,4.55-4.55h7.47"></path><rect class="cls-6" x="599.01" y="970.1" width="11.52" height="104.11"></rect><rect class="cls-5" x="599.01" y="970.1" width="11.52" height="104.11"></rect><path class="cls-2" d="M610.53,816.78V935.32l38.37,11.55a7.85,7.85,0,0,1,5.6,7.53V1107a7.87,7.87,0,0,1-7.87,7.87h-36.1v110.58H1406V816.78Zm775.41,384.75H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66h754.51Z"></path><path class="cls-1" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><path class="cls-7" d="M610.53,816.78V935.32l38.37,11.55a7.85,7.85,0,0,1,5.6,7.53V1107a7.87,7.87,0,0,1-7.87,7.87h-36.1v110.58H1406V816.78Zm775.41,384.75H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66h754.51Z"></path><path class="cls-7" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><path class="cls-7" d="M1385.94,840.45v361.08H631.43a3.66,3.66,0,0,1-3.66-3.66V1138a3.65,3.65,0,0,1,3.66-3.65h28.3a14.7,14.7,0,0,0,14.71-14.71V931a14.7,14.7,0,0,0-14.71-14.71h-28.3a3.65,3.65,0,0,1-3.66-3.66V844.11a3.66,3.66,0,0,1,3.66-3.66Z"></path><line class="cls-7" x1="666.4" y1="1132.79" x2="666.4" y2="1201.53"></line><line class="cls-7" x1="663.66" y1="916.86" x2="663.66" y2="840.45"></line><circle class="cls-8" cx="644.99" cy="878.66" r="10.7"></circle><circle class="cls-8" cx="644.99" cy="1171.55" r="10.7"></circle><circle class="cls-9" cx="644.99" cy="878.66" r="10.7"></circle><circle class="cls-9" cx="644.99" cy="1171.55" r="10.7"></circle><path class="cls-10" d="M1225,1237.05l2.23,11.19a4.25,4.25,0,0,0,4.17,3.42H1249a4.26,4.26,0,0,0,4.18-3.42l2.22-11.19"></path><path class="cls-10" d="M1306.87,1237.05l2.22,11.19a4.26,4.26,0,0,0,4.18,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.22-11.19"></path><path class="cls-10" d="M1388.77,1237.05l2.23,11.19a4.24,4.24,0,0,0,4.17,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.23-11.19"></path><path class="cls-4" d="M1225,1237.05l2.23,11.19a4.25,4.25,0,0,0,4.17,3.42H1249a4.26,4.26,0,0,0,4.18-3.42l2.22-11.19"></path><path class="cls-4" d="M1306.87,1237.05l2.22,11.19a4.26,4.26,0,0,0,4.18,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.22-11.19"></path><path class="cls-4" d="M1388.77,1237.05l2.23,11.19a4.24,4.24,0,0,0,4.17,3.42h17.62a4.25,4.25,0,0,0,4.17-3.42l2.23-11.19"></path><line class="cls-4" x1="1412.9" y1="807.26" x2="1412.9" y2="1237.05"></line></g></svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="479.57 786.58 1040.84 433.17"><defs><style>.cls-1{fill:#cccccb;}.cls-2{fill:#2b2b2b;}.cls-3,.cls-6,.cls-7,.cls-8{fill:none;stroke-miterlimit:10;}.cls-3,.cls-6,.cls-7{stroke:#050606;}.cls-3,.cls-8{stroke-width:1.65px;}.cls-4{fill:#40403f;}.cls-5{fill:#ddd;}.cls-6{stroke-width:1.62px;}.cls-7{stroke-width:1.64px;}.cls-8{stroke:#fff;}.cls-9{fill:#353535;}.cls-10{fill:#c08c2d;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M595,923.44H519.6A10.46,10.46,0,0,1,509.14,913V802.7a15.28,15.28,0,0,1,15.28-15.28h979.89a15.29,15.29,0,0,1,15.29,15.29v400.93a15.3,15.3,0,0,1-15.29,15.29H524.42a15.28,15.28,0,0,1-15.28-15.28V1102.1a10.46,10.46,0,0,1,10.46-10.46H595"></path><rect class="cls-2" x="611.37" y="796.48" width="819.83" height="411.47"></rect><line class="cls-3" x1="1441.99" y1="787.41" x2="1441.99" y2="1218.92"></line><path class="cls-4" d="M620.91,851.7v302.78a1.87,1.87,0,0,1-1.87,1.87h-13.8a8.7,8.7,0,0,1-.89,0,10.23,10.23,0,0,1-9.35-10.2v-286a10.24,10.24,0,0,1,9.35-10.2,8.7,8.7,0,0,1,.89,0H619A1.87,1.87,0,0,1,620.91,851.7Z"></path><rect class="cls-5" x="480.4" y="942.42" width="114.6" height="127.58"></rect><rect class="cls-3" x="480.4" y="942.42" width="114.6" height="127.58"></rect><path class="cls-6" d="M595,923.44H519.6A10.46,10.46,0,0,1,509.14,913V802.7a15.28,15.28,0,0,1,15.28-15.28h979.89a15.29,15.29,0,0,1,15.29,15.29v400.93a15.3,15.3,0,0,1-15.29,15.29H524.42a15.28,15.28,0,0,1-15.28-15.28V1102.1a10.46,10.46,0,0,1,10.46-10.46H595"></path><path class="cls-2" d="M584.65,970.14H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3V973.11A3,3,0,0,1,584.65,970.14Z"></path><rect class="cls-2" x="560.58" y="976.5" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-2" d="M584.65,1026.63H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3v-18.49A3,3,0,0,1,584.65,1026.63Z"></path><rect class="cls-2" x="560.58" y="1028.91" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-3" d="M584.65,970.14H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3V973.11A3,3,0,0,1,584.65,970.14Z"></path><rect class="cls-3" x="560.58" y="976.5" width="9.04" height="16.58" rx="2.72"></rect><path class="cls-3" d="M584.65,1026.63H595a0,0,0,0,1,0,0v24.44a0,0,0,0,1,0,0H584.65a3,3,0,0,1-3-3v-18.49A3,3,0,0,1,584.65,1026.63Z"></path><rect class="cls-3" x="560.58" y="1028.91" width="9.04" height="16.58" rx="2.72"></rect><polyline class="cls-7" points="611.37 1156.35 611.37 1207.95 1431.2 1207.95 1431.2 796.48 611.37 796.48 611.37 849.83"></polyline><line class="cls-3" x1="611.37" y1="1207.95" x2="611.37" y2="1218.93"></line><line class="cls-3" x1="611.37" y1="796.48" x2="611.37" y2="787.42"></line><rect class="cls-8" x="560.58" y="1107.17" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="1107.17" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="1166.28" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="1166.28" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="804.46" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="804.46" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="560.58" y="863.57" width="18.37" height="41.67" rx="4.91"></rect><rect class="cls-8" x="528.51" y="863.57" width="18.37" height="41.67" rx="4.91"></rect><circle class="cls-8" cx="1476.63" cy="831.74" r="27.15"></circle><circle class="cls-8" cx="1476.63" cy="1173.49" r="27.15"></circle><rect class="cls-9" x="676.15" y="804.6" width="742.79" height="396.03"></rect><path class="cls-10" d="M604.35,849.87v306.44a10.23,10.23,0,0,1-9.35-10.2v-286A10.24,10.24,0,0,1,604.35,849.87Z"></path><path class="cls-3" d="M605.24,849.83H619a1.87,1.87,0,0,1,1.87,1.87v302.78a1.87,1.87,0,0,1-1.87,1.87h-13.8A10.24,10.24,0,0,1,595,1146.11v-286A10.24,10.24,0,0,1,605.24,849.83Z"></path><rect class="cls-6" x="676.15" y="804.6" width="742.79" height="396.03"></rect></g></svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.9 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.9 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 83 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 83 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 43 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.3 KiB

View File

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 102 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 71 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 176 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 164 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

View File

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 128 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.0 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 76 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 36 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 31 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="733.42 451.82 573.87 931.48"><defs><style>.cls-1{fill:#8e8d8e;}.cls-2{fill:#383839;}.cls-3{fill:#cccccb;}.cls-4{fill:#222226;}.cls-5,.cls-6{fill:none;stroke:#050606;stroke-miterlimit:10;}.cls-5{stroke-width:1.87px;}.cls-6{stroke-width:3.77px;}.cls-7{fill:#4c4c4d;}</style></defs><g id="Layer_4" data-name="Layer 4"><path class="cls-1" d="M1277.27,847.59h4.35a8.09,8.09,0,0,1,8.09,8.08v138a8.09,8.09,0,0,1-8.09,8.09h-4.35"></path><path class="cls-1" d="M1277.27,732.73h18a10.14,10.14,0,0,1,10.14,10.14v43A10.14,10.14,0,0,1,1295.26,796h-18a0,0,0,0,1,0,0V732.73A0,0,0,0,1,1277.27,732.73Z"></path><path class="cls-2" d="M1256.49,1200.6h0a14.19,14.19,0,0,1-2.83,12.5c-8.13,9.86-19.94,18.58-46,30.75-19.15,9-28.65,16-38.35,29.6a93.15,93.15,0,0,0-8.7,14.61c-6.95,15.17-11.77,44.44-11.77,65.66v3.61a24.09,24.09,0,0,1-24.1,24.09H887.83a24.09,24.09,0,0,1-24.1-24.09v-3.61c0-21.22-4.82-50.49-11.77-65.66a93.15,93.15,0,0,0-8.7-14.61c-9.7-13.63-19.2-20.65-38.35-29.6-26.06-12.17-37.87-20.89-46-30.75a14.22,14.22,0,0,1-2.82-12.5h0"></path><path class="cls-2" d="M756.09,634.53h0a14.19,14.19,0,0,1,2.83-12.5c8.12-9.86,19.93-18.58,46-30.75,19.15-8.95,28.65-16,38.35-29.6a93.15,93.15,0,0,0,8.7-14.61c6.95-15.17,11.77-44.44,11.77-65.66V477.8a24.09,24.09,0,0,1,24.1-24.09h236.92a24.09,24.09,0,0,1,24.1,24.09v3.61c0,21.22,4.82,50.49,11.77,65.66a93.15,93.15,0,0,0,8.7,14.61c9.7,13.63,19.2,20.65,38.35,29.6,26,12.17,37.86,20.89,46,30.75a14.19,14.19,0,0,1,2.83,12.5h0"></path><rect class="cls-3" x="735.31" y="598.25" width="541.96" height="638.99" rx="96.44"></rect><path class="cls-2" d="M1247.38,694.68v446.11a66.63,66.63,0,0,1-66.54,66.56H831.75a66.62,66.62,0,0,1-66.56-66.56V694.68a66.63,66.63,0,0,1,66.56-66.55h349.09A66.64,66.64,0,0,1,1247.38,694.68Z"></path><rect class="cls-4" x="817.71" y="721.76" width="379.03" height="388.6"></rect><path class="cls-5" d="M1247.38,694.68v446.11a66.63,66.63,0,0,1-66.54,66.56H831.75a66.62,66.62,0,0,1-66.56-66.56V694.68a66.63,66.63,0,0,1,66.56-66.55h349.09A66.64,66.64,0,0,1,1247.38,694.68Z"></path><rect class="cls-6" x="735.31" y="598.25" width="541.96" height="638.99" rx="96.44"></rect><path class="cls-6" d="M1256.49,1200.6h0a14.19,14.19,0,0,1-2.83,12.5c-8.13,9.86-19.94,18.58-46,30.75-19.15,9-28.65,16-38.35,29.6a93.15,93.15,0,0,0-8.7,14.61c-6.95,15.17-11.77,44.44-11.77,65.66v3.61a24.09,24.09,0,0,1-24.1,24.09H887.83a24.09,24.09,0,0,1-24.1-24.09v-3.61c0-21.22-4.82-50.49-11.77-65.66a93.15,93.15,0,0,0-8.7-14.61c-9.7-13.63-19.2-20.65-38.35-29.6-26.06-12.17-37.87-20.89-46-30.75a14.22,14.22,0,0,1-2.82-12.5h0"></path><path class="cls-6" d="M756.09,634.53h0a14.19,14.19,0,0,1,2.83-12.5c8.12-9.86,19.93-18.58,46-30.75,19.15-8.95,28.65-16,38.35-29.6a93.15,93.15,0,0,0,8.7-14.61c6.95-15.17,11.77-44.44,11.77-65.66V477.8a24.09,24.09,0,0,1,24.1-24.09h236.92a24.09,24.09,0,0,1,24.1,24.09v3.61c0,21.22,4.82,50.49,11.77,65.66a93.15,93.15,0,0,0,8.7,14.61c9.7,13.63,19.2,20.65,38.35,29.6,26,12.17,37.86,20.89,46,30.75a14.19,14.19,0,0,1,2.83,12.5h0"></path><rect class="cls-5" x="817.71" y="721.76" width="379.03" height="388.6"></rect><path class="cls-6" d="M1277.27,847.59h4.35a8.09,8.09,0,0,1,8.09,8.08v138a8.09,8.09,0,0,1-8.09,8.09h-4.35"></path><path class="cls-6" d="M1277.27,732.73h18a10.14,10.14,0,0,1,10.14,10.14v43A10.14,10.14,0,0,1,1295.26,796h-18a0,0,0,0,1,0,0V732.73A0,0,0,0,1,1277.27,732.73Z"></path><circle class="cls-7" cx="1083.08" cy="1177.35" r="16.6"></circle><rect class="cls-2" x="1280.24" y="739.77" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="750.91" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="762.06" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="773.2" width="16.77" height="4.59" rx="2.29"></rect><rect class="cls-2" x="1280.24" y="784.34" width="16.77" height="4.59" rx="2.29"></rect></g></svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 70 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -1,109 +0,0 @@
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg105" sodipodi:docname="thinknode_m1.svg" inkscape:version="1.4 (e7c3feb1, 2024-10-09)" viewBox="397.31 77.24 361 863.17">
<sodipodi:namedview id="namedview105" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="4.066786" inkscape:cx="564.94244" inkscape:cy="741.49463" inkscape:window-width="1472" inkscape:window-height="890" inkscape:window-x="0" inkscape:window-y="38" inkscape:window-maximized="1" inkscape:current-layer="Layer_3"/>
<defs id="defs1">
<style id="style1">.cls-1{fill:#353535;}.cls-2{fill:#262626;}.cls-3{fill:#cccccb;}.cls-4{fill:#2b2b2b;}.cls-5{fill:#f05043;}.cls-6{fill:#3d3d3d;}.cls-7{fill:#231f20;}.cls-8{fill:none;stroke:#000;stroke-miterlimit:10;}</style>
</defs>
<g id="Layer_3" data-name="Layer 3">
<path class="cls-1" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path1"/>
<rect class="cls-2" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect1"/>
<rect class="cls-1" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect2"/>
<rect class="cls-3" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect3"/>
<path class="cls-1" d="M507.38,77.74H472.16a7,7,0,0,0-7,7V359.93L452.2,396.26v39.91H561V396.26l-13.3-36V84.15a6.41,6.41,0,0,0-6.41-6.41Z" id="path3"/>
<rect class="cls-2" x="454.25" y="436.17" width="104.38" height="3.65" id="rect4"/>
<polygon class="cls-1" points="442.68 449.91 448.16 440.69 562.98 440.69 570.51 449.91 442.68 449.91" id="polygon4"/>
<rect class="cls-1" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.8" id="rect5"/>
<path class="cls-2" d="M611.2,356v-5.48a3.13,3.13,0,0,1,3.13-3.13h86.35a3.13,3.13,0,0,1,3.13,3.13V356Z" id="path5"/>
<rect class="cls-2" x="611.07" y="416.61" width="92.74" height="23.22" id="rect6"/>
<polygon class="cls-1" points="592.99 449.91 598.47 440.69 713.42 440.69 720.82 449.91 592.99 449.91" id="polygon6"/>
<rect class="cls-2" x="751.94" y="555.13" width="5.87" height="47.48" id="rect7"/>
<path class="cls-2" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path7"/>
<path class="cls-2" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path8"/>
<path class="cls-4" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path9"/>
<rect class="cls-2" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect9"/>
<circle class="cls-5" cx="518.72" cy="853.89" r="5.48" id="circle9"/>
<circle class="cls-1" cx="640.14" cy="853.89" r="5.48" id="circle10"/>
<circle class="cls-1" cx="541.83" cy="853.89" r="5.48" id="circle11"/>
<circle class="cls-1" cx="567.67" cy="853.89" r="5.48" id="circle12"/>
<circle class="cls-1" cx="593.51" cy="853.89" r="5.48" id="circle13"/>
<circle class="cls-1" cx="616.82" cy="853.89" r="5.48" id="circle14"/>
<path class="cls-4" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path14"/>
<path class="cls-4" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path15"/>
<path class="cls-6" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path16"/>
<path class="cls-6" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path17"/>
<path class="cls-6" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path18"/>
<path class="cls-6" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path19"/>
<path class="cls-6" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path20"/>
<path class="cls-6" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path21"/>
<path class="cls-1" d="M477,924.32h-3V903.09h-7.46v-2.65h17.9v2.65H477Z" id="path51"/>
<path class="cls-1" d="M490.59,906.36c0,.43,0,.86,0,1.31s-.07.85-.12,1.2h.2a5.17,5.17,0,0,1,1.44-1.54,7.08,7.08,0,0,1,1.94-.92,7.81,7.81,0,0,1,2.21-.31,8.61,8.61,0,0,1,3.63.68,4.62,4.62,0,0,1,2.19,2.13,8.22,8.22,0,0,1,.73,3.74v11.67h-2.9V912.85a4.72,4.72,0,0,0-1-3.24,3.92,3.92,0,0,0-3.05-1.07,5.65,5.65,0,0,0-3.14.75,4.07,4.07,0,0,0-1.62,2.21,11.17,11.17,0,0,0-.49,3.56v9.26h-2.94V898.91h2.94Z" id="path52"/>
<path class="cls-1" d="M509.82,899.67a1.73,1.73,0,0,1,1.19.46,2.17,2.17,0,0,1,0,2.82,1.74,1.74,0,0,1-1.19.47,1.77,1.77,0,0,1-1.24-.47,2.24,2.24,0,0,1,0-2.82A1.76,1.76,0,0,1,509.82,899.67Zm1.44,6.73v17.92h-2.94V906.4Z" id="path53"/>
<path class="cls-1" d="M525.58,906.06a6.8,6.8,0,0,1,4.85,1.56c1.09,1,1.63,2.71,1.63,5v11.67h-2.91V912.85a4.67,4.67,0,0,0-1-3.24,3.88,3.88,0,0,0-3-1.07c-2,0-3.36.56-4.11,1.67a8.54,8.54,0,0,0-1.14,4.82v9.29H517V906.4h2.37l.44,2.44h.16a5.68,5.68,0,0,1,1.49-1.56,6.41,6.41,0,0,1,2-.92A8.13,8.13,0,0,1,525.58,906.06Z" id="path54"/>
<path class="cls-1" d="M540.53,912.18c0,.36,0,.83,0,1.41s-.07,1.08-.09,1.5h.14l.6-.77.82-1c.28-.34.52-.63.72-.85l5.72-6.05h3.44l-7.26,7.66,7.76,10.26h-3.54L542.57,916l-2,1.78v6.58h-2.91V898.91h2.91Z" id="path55"/>
<path class="cls-1" d="M574.81,924.32H571.3l-12.78-19.83h-.13c0,.51.08,1.12.11,1.82s.07,1.45.1,2.24,0,1.61,0,2.43v13.34h-2.77V900.44h3.48l12.74,19.77h.13c0-.36-.05-.89-.08-1.6s-.07-1.5-.1-2.35,0-1.62,0-2.34V900.44h2.81Z" id="path56"/>
<path class="cls-1" d="M596.48,915.33a12.32,12.32,0,0,1-.58,4,8.32,8.32,0,0,1-1.67,2.93,7,7,0,0,1-2.65,1.82,9.31,9.31,0,0,1-3.46.62,8.63,8.63,0,0,1-3.28-.62,7.29,7.29,0,0,1-2.61-1.82,8.57,8.57,0,0,1-1.72-2.93,11.76,11.76,0,0,1-.62-4,11.43,11.43,0,0,1,1-5,7.12,7.12,0,0,1,2.87-3.14,8.76,8.76,0,0,1,4.45-1.09,8.4,8.4,0,0,1,4.3,1.09,7.45,7.45,0,0,1,2.91,3.14A11,11,0,0,1,596.48,915.33Zm-13.54,0a10.93,10.93,0,0,0,.55,3.66,4.82,4.82,0,0,0,1.72,2.39,5.69,5.69,0,0,0,5.95,0,4.78,4.78,0,0,0,1.73-2.39,10.93,10.93,0,0,0,.55-3.66,10.36,10.36,0,0,0-.57-3.65,4.84,4.84,0,0,0-1.72-2.32,5,5,0,0,0-3-.82,4.5,4.5,0,0,0-4,1.8A8.79,8.79,0,0,0,582.94,915.33Z" id="path57"/>
<path class="cls-1" d="M607.49,924.66a6.67,6.67,0,0,1-5.35-2.33c-1.34-1.54-2-3.86-2-6.94s.67-5.4,2-7a6.74,6.74,0,0,1,5.37-2.36,7.71,7.71,0,0,1,2.44.35,6.37,6.37,0,0,1,1.81,1,6.58,6.58,0,0,1,1.3,1.34h.2c0-.29-.06-.72-.11-1.29s-.09-1-.09-1.36v-7.15H616v25.41h-2.38l-.43-2.4h-.14a6.51,6.51,0,0,1-1.3,1.38,5.9,5.9,0,0,1-1.82,1A7.4,7.4,0,0,1,607.49,924.66Zm.46-2.44c1.9,0,3.23-.52,4-1.56a7.84,7.84,0,0,0,1.16-4.7v-.53a9.84,9.84,0,0,0-1.11-5.14c-.73-1.19-2.09-1.79-4.08-1.79a4,4,0,0,0-3.56,1.89,9.46,9.46,0,0,0-1.19,5.07,9,9,0,0,0,1.19,5A4,4,0,0,0,608,922.22Z" id="path58"/>
<path class="cls-1" d="M628.62,906.06a7.53,7.53,0,0,1,4,1,6.62,6.62,0,0,1,2.54,2.82,9.69,9.69,0,0,1,.89,4.27v1.77H623.74a6.75,6.75,0,0,0,1.56,4.63,5.42,5.42,0,0,0,4.16,1.59,12.58,12.58,0,0,0,3-.32,16.78,16.78,0,0,0,2.72-.92v2.58a13.84,13.84,0,0,1-2.71.89,16.15,16.15,0,0,1-3.17.28,9.46,9.46,0,0,1-4.5-1,7.15,7.15,0,0,1-3-3.09,10.61,10.61,0,0,1-1.09-5,12,12,0,0,1,1-5,7.33,7.33,0,0,1,6.94-4.38Zm0,2.41a4.26,4.26,0,0,0-3.33,1.36,6.34,6.34,0,0,0-1.45,3.76h9.13a7.05,7.05,0,0,0-.47-2.68,3.94,3.94,0,0,0-1.42-1.79A4.33,4.33,0,0,0,628.59,908.47Z" id="path59"/>
<path class="cls-1" d="M639.36,913h8.1v2.74h-8.1Z" id="path60"/>
<path class="cls-1" d="M662.87,924.32,655,903.39h-.13c0,.44.08,1,.12,1.7s.06,1.45.08,2.26,0,1.65,0,2.49v14.48h-2.77V900.44h4.45L664.15,920h.13l7.49-19.57h4.42v23.88h-3V909.64c0-.78,0-1.55,0-2.32s.06-1.5.1-2.18.08-1.25.1-1.72h-.13l-8,20.9Z" id="path61"/>
<path class="cls-1" d="M688.16,924.32V909.41c0-.63,0-1.3,0-2s0-1.42.07-2.1,0-1.27,0-1.76c-.35.38-.66.69-.92.92s-.6.54-1,.92l-2.41,2-1.57-2,6.26-4.89H691v23.88Z" id="path62"/>
<path class="cls-1" d="M502.55,894.19l-2.22-2.37a14.1,14.1,0,0,1,18.94-.33l-2.13,2.44a10.9,10.9,0,0,0-7.16-2.69A10.78,10.78,0,0,0,502.55,894.19Z" id="path63"/>
<path class="cls-1" d="M506.38,897.85l-2.4-2.18a8.08,8.08,0,0,1,11.61-.39l-2.24,2.33a4.85,4.85,0,0,0-7,.24Z" id="path64"/>
</g>
<g id="Layer_2" data-name="Layer 2">
<path class="cls-8" d="M472.55,77.74h68.09a7.43,7.43,0,0,1,7.43,7.43V360.26a0,0,0,0,1,0,0h-83a0,0,0,0,1,0,0V85.17A7.43,7.43,0,0,1,472.55,77.74Z" id="path65"/>
<line class="cls-8" x1="465.12" y1="123.91" x2="548.07" y2="123.91" id="line65"/>
<line class="cls-8" x1="465.12" y1="149.74" x2="548.07" y2="149.74" id="line66"/>
<polyline class="cls-8" points="465.12 360.26 452.2 396.26 452.2 436.17 560.99 436.17 560.99 396.26 548.07 360.26" id="polyline66"/>
<line class="cls-8" x1="452.2" y1="396.26" x2="560.98" y2="396.26" id="line67"/>
<path class="cls-8" d="M449.69,440.17H562a3.26,3.26,0,0,1,2.56,1.55l5.93,8.19H442.68l4-7.49A3.65,3.65,0,0,1,449.69,440.17Z" id="path67"/>
<path class="cls-8" d="M600,440.17H712.33a3.24,3.24,0,0,1,2.56,1.55l5.93,8.19H593l4-7.49A3.65,3.65,0,0,1,600,440.17Z" id="path68"/>
<line class="cls-8" x1="454.45" y1="436.17" x2="454.45" y2="439.83" id="line68"/>
<line class="cls-8" x1="558.64" y1="436.17" x2="558.64" y2="439.83" id="line69"/>
<rect class="cls-8" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.87" id="rect69"/>
<line class="cls-8" x1="611.07" y1="416.61" x2="611.07" y2="439.83" id="line70"/>
<line class="cls-8" x1="703.81" y1="416.61" x2="703.81" y2="439.83" id="line71"/>
<path class="cls-8" d="M614.2,347.35h86.48a3.13,3.13,0,0,1,3.13,3.13V356a0,0,0,0,1,0,0H611.07a0,0,0,0,1,0,0v-5.48A3.13,3.13,0,0,1,614.2,347.35Z" id="path71"/>
<line class="cls-8" x1="570.51" y1="449.91" x2="592.99" y2="449.91" id="line72"/>
<path class="cls-8" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path72"/>
<rect class="cls-8" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect72"/>
<rect class="cls-8" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect73"/>
<rect class="cls-8" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect74"/>
<path class="cls-8" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path74"/>
<path class="cls-8" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path75"/>
<path class="cls-8" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path76"/>
<path class="cls-8" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path77"/>
<path class="cls-8" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path78"/>
<path class="cls-8" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path79"/>
<rect class="cls-8" x="751.94" y="555.13" width="5.87" height="47.48" id="rect79"/>
<path class="cls-8" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path80"/>
<path class="cls-8" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path81"/>
<path class="cls-8" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path82"/>
<rect class="cls-8" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect82"/>
<circle class="cls-8" cx="518.72" cy="853.89" r="5.48" id="circle82"/>
<circle class="cls-8" cx="640.14" cy="853.89" r="5.48" id="circle83"/>
<circle class="cls-8" cx="541.83" cy="853.89" r="5.48" id="circle84"/>
<circle class="cls-8" cx="567.67" cy="853.89" r="5.48" id="circle85"/>
<circle class="cls-8" cx="593.51" cy="853.89" r="5.48" id="circle86"/>
<circle class="cls-8" cx="616.82" cy="853.89" r="5.48" id="circle87"/>
<line class="cls-8" x1="430.68" y1="572.74" x2="430.68" y2="602.61" id="line87"/>
<line class="cls-8" x1="424.42" y1="595.43" x2="424.42" y2="578.11" id="line88"/>
<line class="cls-8" x1="438.21" y1="595.43" x2="438.21" y2="578.11" id="line89"/>
<line class="cls-8" x1="430.68" y1="644.74" x2="430.68" y2="674.61" id="line90"/>
<line class="cls-8" x1="424.42" y1="667.43" x2="424.42" y2="650.11" id="line91"/>
<line class="cls-8" x1="438.21" y1="667.43" x2="438.21" y2="650.11" id="line92"/>
<line class="cls-8" x1="430.68" y1="716.74" x2="430.68" y2="746.61" id="line93"/>
<line class="cls-8" x1="424.42" y1="739.43" x2="424.42" y2="722.11" id="line94"/>
<line class="cls-8" x1="438.21" y1="739.43" x2="438.21" y2="722.11" id="line95"/>
<line class="cls-8" x1="730.03" y1="572.74" x2="730.03" y2="602.61" id="line96"/>
<line class="cls-8" x1="723.77" y1="595.43" x2="723.77" y2="578.11" id="line97"/>
<line class="cls-8" x1="737.56" y1="595.43" x2="737.56" y2="578.11" id="line98"/>
<line class="cls-8" x1="730.03" y1="644.74" x2="730.03" y2="674.61" id="line99"/>
<line class="cls-8" x1="723.77" y1="667.43" x2="723.77" y2="650.11" id="line100"/>
<line class="cls-8" x1="737.56" y1="667.43" x2="737.56" y2="650.11" id="line101"/>
<line class="cls-8" x1="730.03" y1="716.74" x2="730.03" y2="746.61" id="line102"/>
<line class="cls-8" x1="723.77" y1="739.43" x2="723.77" y2="722.11" id="line103"/>
<line class="cls-8" x1="737.56" y1="739.43" x2="737.56" y2="722.11" id="line104"/>
<path class="cls-8" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path104"/>
<path class="cls-8" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path105"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,391 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="svg75"
sodipodi:docname="thinknode_m2.svg"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
viewBox="388.5 121.73 413.05 787.86"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview75"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="3.7680002"
inkscape:cx="265.65816"
inkscape:cy="681.92672"
inkscape:window-width="1472"
inkscape:window-height="890"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_3" />
<defs
id="defs1">
<style
id="style1">.cls-1{fill:#262626;}.cls-2{fill:#353535;}.cls-3{fill:#303030;}.cls-4{fill:#f05043;}.cls-5,.cls-6,.cls-7,.cls-8{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-6{stroke-width:0.88px;}.cls-7{stroke-width:0.95px;}.cls-8{stroke-width:1px;}.cls-9{fill:#acdee5;}</style>
</defs>
<g
id="Layer_3"
data-name="Layer 3">
<polygon
class="cls-1"
points="575.63 361.13 575.34 354.09 574.7 338.7 574.41 331.65 479.53 331.65 479.23 338.7 478.57 354.09 478.26 361.13 575.63 361.13"
id="polygon1" />
<polyline
class="cls-2"
points="458.33 403 473.46 384.87 579.68 384.87 595.03 403"
id="polyline1" />
<path
class="cls-2"
d="M579.68,384.87l-.87-21.35a2.51,2.51,0,0,0-2.5-2.39H477.56a2.5,2.5,0,0,0-2.49,2.39l-.87,21.35"
id="path1" />
<path
class="cls-2"
d="M578.32,351.57,577.88,341a2.41,2.41,0,0,0-2.41-2.32H478.41A2.42,2.42,0,0,0,476,341l-.43,10.55a2.42,2.42,0,0,0,2.42,2.52H575.9A2.43,2.43,0,0,0,578.32,351.57Z"
id="path2" />
<path
class="cls-2"
d="M476.46,329.56a2,2,0,0,0,2,2.09h96.94a2,2,0,0,0,2-2.09l-7.89-193.08a14.91,14.91,0,0,0-14.9-14.31H499.26a14.91,14.91,0,0,0-14.9,14.31Z"
id="path3" />
<polyline
class="cls-2"
points="491.72 331.65 499.03 137.3 555.9 137.3 561.64 331.65"
id="polyline3" />
<rect
class="cls-1"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect3" />
<rect
class="cls-2"
x="405.9"
y="417.86"
width="372.52"
height="471.54"
rx="38.16"
id="rect4" />
<path
class="cls-3"
d="M763.35,521.78v329A23.38,23.38,0,0,1,740,874.13H441.22a23.38,23.38,0,0,1-23.38-23.38V456.51a23.38,23.38,0,0,1,23.38-23.38H675.28A18,18,0,0,1,688,438.41L757.66,508A19.43,19.43,0,0,1,763.35,521.78Z"
id="path4" />
<path
class="cls-1"
d="M716.86,532.78H462.77a18,18,0,0,0-18,18V673.91a18,18,0,0,0,18,18H716.86a18,18,0,0,0,18-18V550.78A18,18,0,0,0,716.86,532.78Zm6.52,137.48a10.7,10.7,0,0,1-10.7,10.7H465.38a10.7,10.7,0,0,1-10.7-10.7V551.83a10.7,10.7,0,0,1,10.7-10.7h247.3a10.7,10.7,0,0,1,10.7,10.7Z"
id="path5" />
<rect
x="454.68"
y="541.13"
width="268.7"
height="139.83"
rx="10.7"
id="rect5" />
<path
class="cls-3"
d="M447,904.26v2.94a2,2,0,0,0,2,1.95h14.28a2,2,0,0,0,2-1.95v-2.94"
id="path6" />
<path
class="cls-3"
d="M718.74,904.26v2.94a2,2,0,0,0,2,1.95H735a2,2,0,0,0,2-1.95v-2.94"
id="path7" />
<path
class="cls-3"
d="M790.16,508.83h.39a10.56,10.56,0,0,1,10.56,10.56V656.57a10.56,10.56,0,0,1-10.56,10.56h-.39"
id="path8" />
<path
class="cls-3"
d="M394.16,518.17h-3.31a1.91,1.91,0,0,0-1.91,1.91V797a1.9,1.9,0,0,0,1.91,1.91h3.31"
id="path9" />
<rect
class="cls-1"
x="502.29"
y="782"
width="180.26"
height="24.65"
rx="12.33"
id="rect9" />
<circle
class="cls-4"
cx="515.99"
cy="794.33"
r="5.48"
id="circle9" />
<circle
class="cls-2"
cx="637.4"
cy="794.33"
r="5.48"
id="circle10" />
<circle
class="cls-2"
cx="660.08"
cy="794.33"
r="5.48"
id="circle11" />
<circle
class="cls-2"
cx="539.09"
cy="794.33"
r="5.48"
id="circle12" />
<circle
class="cls-2"
cx="564.93"
cy="794.33"
r="5.48"
id="circle13" />
<circle
class="cls-2"
cx="590.77"
cy="794.33"
r="5.48"
id="circle14" />
<circle
class="cls-2"
cx="614.09"
cy="794.33"
r="5.48"
id="circle15" />
<path
class="cls-1"
d="M475.77,856.71h-3.41V832.6h-8.47v-3H484.2v3h-8.43Z"
id="path15" />
<path
class="cls-1"
d="M491.19,836.32c0,.48,0,1-.06,1.48s-.08,1-.13,1.37h.23a5.69,5.69,0,0,1,1.63-1.75,7.82,7.82,0,0,1,2.2-1,8.81,8.81,0,0,1,2.51-.36,9.64,9.64,0,0,1,4.12.78,5.2,5.2,0,0,1,2.49,2.41,9.43,9.43,0,0,1,.83,4.25v13.25h-3.3v-13a5.37,5.37,0,0,0-1.1-3.69,4.46,4.46,0,0,0-3.46-1.21,6.41,6.41,0,0,0-3.57.85,4.68,4.68,0,0,0-1.84,2.51,12.81,12.81,0,0,0-.55,4v10.52h-3.34V827.85h3.34Z"
id="path16" />
<path
class="cls-1"
d="M513,828.73a2,2,0,0,1,1.35.51,2,2,0,0,1,.59,1.61,2.07,2.07,0,0,1-.59,1.6A2,2,0,0,1,513,833a2,2,0,0,1-1.4-.53,2.1,2.1,0,0,1-.57-1.6,2.06,2.06,0,0,1,.57-1.61A2,2,0,0,1,513,828.73Zm1.64,7.63v20.35h-3.35V836.36Z"
id="path17" />
<path
class="cls-1"
d="M530.91,836a7.69,7.69,0,0,1,5.5,1.77c1.24,1.17,1.86,3.08,1.86,5.71v13.25H535v-13a5.37,5.37,0,0,0-1.1-3.69,4.46,4.46,0,0,0-3.46-1.21q-3.37,0-4.67,1.9a9.7,9.7,0,0,0-1.29,5.47v10.55h-3.34V836.36h2.7l.49,2.77h.19a6.17,6.17,0,0,1,1.69-1.76,7.31,7.31,0,0,1,2.22-1A9.16,9.16,0,0,1,530.91,836Z"
id="path18" />
<path
class="cls-1"
d="M547.88,842.93q0,.6-.06,1.59t-.09,1.71h.15l.68-.87.93-1.16c.32-.39.59-.72.82-1l6.49-6.87h3.91l-8.24,8.69,8.81,11.66h-4l-7.06-9.49-2.32,2v7.48h-3.3V827.85h3.3Z"
id="path19" />
<path
class="cls-1"
d="M586.8,856.71h-4l-14.5-22.52h-.15c.05.59.09,1.28.13,2.07s.07,1.65.11,2.55.06,1.81.06,2.75v15.15h-3.15V829.6h4L583.72,852h.16c0-.4-.06-1-.1-1.82s-.08-1.7-.11-2.66-.06-1.85-.06-2.66V829.6h3.19Z"
id="path20" />
<path
class="cls-1"
d="M611.4,846.5a14.11,14.11,0,0,1-.66,4.5,9.41,9.41,0,0,1-1.9,3.32,7.92,7.92,0,0,1-3,2.07,10.53,10.53,0,0,1-3.93.7,9.66,9.66,0,0,1-3.72-.7,8.18,8.18,0,0,1-3-2.07,9.67,9.67,0,0,1-2-3.32,13.29,13.29,0,0,1-.7-4.5,13,13,0,0,1,1.14-5.72,8.25,8.25,0,0,1,3.26-3.57A10,10,0,0,1,602,836a9.47,9.47,0,0,1,4.87,1.23,8.61,8.61,0,0,1,3.31,3.57A12.41,12.41,0,0,1,611.4,846.5Zm-15.37,0a12.33,12.33,0,0,0,.62,4.15,5.45,5.45,0,0,0,2,2.72,6.49,6.49,0,0,0,6.76,0,5.5,5.5,0,0,0,2-2.72,12.32,12.32,0,0,0,.63-4.15,11.76,11.76,0,0,0-.65-4.14,5.53,5.53,0,0,0-1.95-2.64,5.76,5.76,0,0,0-3.4-.93,5.08,5.08,0,0,0-4.52,2.05A9.87,9.87,0,0,0,596,846.5Z"
id="path21" />
<path
class="cls-1"
d="M623.9,857.09a7.62,7.62,0,0,1-6.08-2.64c-1.52-1.76-2.28-4.38-2.28-7.88s.77-6.13,2.3-7.91a7.61,7.61,0,0,1,6.09-2.68,8.58,8.58,0,0,1,2.78.4,6.79,6.79,0,0,1,2,1.08,7.87,7.87,0,0,1,1.48,1.52h.22c0-.33-.07-.82-.13-1.46s-.09-1.16-.09-1.54v-8.13h3.34v28.86h-2.7l-.49-2.73h-.15a7.83,7.83,0,0,1-1.48,1.57,6.86,6.86,0,0,1-2.07,1.12A8.44,8.44,0,0,1,623.9,857.09Zm.53-2.77q3.23,0,4.53-1.77a8.85,8.85,0,0,0,1.31-5.33v-.61a11.15,11.15,0,0,0-1.25-5.83c-.83-1.35-2.38-2-4.63-2a4.45,4.45,0,0,0-4,2.15,10.68,10.68,0,0,0-1.35,5.75,10.12,10.12,0,0,0,1.35,5.66A4.56,4.56,0,0,0,624.43,854.32Z"
id="path22" />
<path
class="cls-1"
d="M647.89,836a8.5,8.5,0,0,1,4.5,1.14,7.45,7.45,0,0,1,2.89,3.21,11,11,0,0,1,1,4.84v2H642.35a7.71,7.71,0,0,0,1.76,5.26,6.19,6.19,0,0,0,4.73,1.8,14.83,14.83,0,0,0,3.44-.36,19.71,19.71,0,0,0,3.09-1v2.92a16,16,0,0,1-3.07,1,18.05,18.05,0,0,1-3.61.32,10.7,10.7,0,0,1-5.11-1.18,8.12,8.12,0,0,1-3.45-3.51,12,12,0,0,1-1.24-5.71A13.57,13.57,0,0,1,640,841a8.32,8.32,0,0,1,7.88-5Zm0,2.73a4.83,4.83,0,0,0-3.77,1.54,7.25,7.25,0,0,0-1.66,4.27h10.37a8,8,0,0,0-.53-3,4.49,4.49,0,0,0-1.61-2A4.92,4.92,0,0,0,647.85,838.71Z"
id="path23" />
<path
class="cls-1"
d="M660.08,843.88h9.19V847h-9.19Z"
id="path24" />
<path
class="cls-1"
d="M686.77,856.71l-8.92-23.77h-.15c0,.51.09,1.15.13,1.94s.07,1.64.1,2.56,0,1.87,0,2.83v16.44h-3.15V829.6h5.05l8.36,22.21h.15l8.5-22.21h5v27.11h-3.38V840q0-1.32,0-2.64t.12-2.46c.05-.78.09-1.43.11-2h-.15l-9,23.73Z"
id="path25" />
<path
class="cls-1"
d="M504.76,822.5l-2.52-2.69a16,16,0,0,1,21.51-.37l-2.42,2.77a12.35,12.35,0,0,0-16.57.29Z"
id="path27" />
<path
class="cls-1"
d="M509.12,826.65l-2.73-2.47a9.18,9.18,0,0,1,13.18-.45L517,826.38a5.51,5.51,0,0,0-7.9.27Z"
id="path28" />
<path
d="M 727.48499,857.1333 H 709.11 v -3.80989 q 1.91406,-1.64063 3.82812,-3.28125 1.93229,-1.64062 3.59115,-3.26302 3.49999,-3.39062 4.79426,-5.3776 1.29427,-2.0052 1.29427,-4.32031 0,-2.11458 -1.40364,-3.29947 -1.38542,-1.20313 -3.88281,-1.20313 -1.65885,0 -3.59114,0.58334 -1.93229,0.58333 -3.77344,1.78645 h -0.18229 v -3.82812 q 1.29427,-0.63802 3.44531,-1.16666 2.16927,-0.52865 4.19271,-0.52865 4.17447,0 6.54426,2.02344 2.36979,2.0052 2.36979,5.45051 0,1.54948 -0.40104,2.89844 -0.38281,1.33073 -1.14844,2.53385 -0.71093,1.13021 -1.67708,2.22396 -0.94791,1.09375 -2.3151,2.42447 -1.95052,1.91406 -4.02864,3.71875 -2.07813,1.78646 -3.88281,3.31771 h 14.60155 z"
id="text1"
style="font-size:37.3333px;fill:#262626"
aria-label="2" />
</g>
<g
id="Layer_2"
data-name="Layer 2">
<rect
class="cls-5"
x="502.29"
y="782"
width="180.26"
height="24.65"
rx="12.33"
id="rect28" />
<circle
class="cls-5"
cx="515.99"
cy="794.33"
r="5.48"
id="circle28" />
<circle
class="cls-5"
cx="637.4"
cy="794.33"
r="5.48"
id="circle29" />
<circle
class="cls-5"
cx="660.08"
cy="794.33"
r="5.48"
id="circle30" />
<circle
class="cls-5"
cx="539.09"
cy="794.33"
r="5.48"
id="circle31" />
<circle
class="cls-5"
cx="564.93"
cy="794.33"
r="5.48"
id="circle32" />
<circle
class="cls-5"
cx="590.77"
cy="794.33"
r="5.48"
id="circle33" />
<circle
class="cls-5"
cx="614.09"
cy="794.33"
r="5.48"
id="circle34" />
<path
class="cls-6"
d="M763.35,521.78v329A23.38,23.38,0,0,1,740,874.13H441.22a23.38,23.38,0,0,1-23.38-23.38V456.51a23.38,23.38,0,0,1,23.38-23.38H675.28A18,18,0,0,1,688,438.41L757.66,508A19.43,19.43,0,0,1,763.35,521.78Z"
id="path34" />
<rect
class="cls-7"
x="405.9"
y="417.86"
width="372.52"
height="471.54"
rx="38.16"
id="rect34" />
<rect
class="cls-8"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect35" />
<path
class="cls-6"
d="M763.35,462.15v30.34a5.64,5.64,0,0,1-9.62,4L700,442.75a5.63,5.63,0,0,1,4-9.62h30.34A29,29,0,0,1,763.35,462.15Z"
id="path35" />
<rect
class="cls-6"
x="444.77"
y="532.78"
width="290.09"
height="159.13"
rx="18"
id="rect36" />
<rect
class="cls-6"
x="454.68"
y="541.13"
width="268.7"
height="139.83"
rx="10.7"
id="rect37" />
<path
class="cls-6"
d="M447,904.26v2.94a2,2,0,0,0,2,1.95h14.28a2,2,0,0,0,2-1.95v-2.94"
id="path37" />
<path
class="cls-6"
d="M718.74,904.26v2.94a2,2,0,0,0,2,1.95H735a2,2,0,0,0,2-1.95v-2.94"
id="path38" />
<path
class="cls-6"
d="M790.16,508.83h.39a10.56,10.56,0,0,1,10.56,10.56V656.57a10.56,10.56,0,0,1-10.56,10.56h-.39"
id="path39" />
<path
class="cls-6"
d="M394.16,518.17h-3.31a1.91,1.91,0,0,0-1.91,1.91V797a1.9,1.9,0,0,0,1.91,1.91h3.31"
id="path40" />
<polyline
class="cls-6"
points="458.33 403 473.46 384.87 579.68 384.87 595.03 403"
id="polyline40" />
<path
class="cls-6"
d="M579.68,384.87l-.87-21.35a2.51,2.51,0,0,0-2.5-2.39H477.56a2.5,2.5,0,0,0-2.49,2.39l-.87,21.35"
id="path41" />
<path
class="cls-6"
d="M578.32,351.57,577.88,341a2.41,2.41,0,0,0-2.41-2.32H478.41A2.42,2.42,0,0,0,476,341l-.43,10.55a2.42,2.42,0,0,0,2.42,2.52H575.9A2.43,2.43,0,0,0,578.32,351.57Z"
id="path42" />
<path
class="cls-6"
d="M476.46,329.56a2,2,0,0,0,2,2.09h96.94a2,2,0,0,0,2-2.09l-7.89-193.08a14.91,14.91,0,0,0-14.9-14.31H499.26a14.91,14.91,0,0,0-14.9,14.31Z"
id="path43" />
<line
class="cls-6"
x1="479.53"
y1="331.65"
x2="479.23"
y2="338.7"
id="line43" />
<line
class="cls-6"
x1="478.57"
y1="354.09"
x2="478.26"
y2="361.13"
id="line44" />
<line
class="cls-6"
x1="574.7"
y1="338.7"
x2="574.41"
y2="331.65"
id="line45" />
<line
class="cls-6"
x1="575.63"
y1="361.13"
x2="575.34"
y2="354.09"
id="line46" />
<polyline
class="cls-6"
points="491.72 331.65 499.03 137.3 555.9 137.3 561.64 331.65"
id="polyline46" />
<rect
class="cls-8"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect46" />
<rect
class="cls-8"
x="394.16"
y="403"
width="396"
height="501.26"
rx="48.72"
id="rect47" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -1,160 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
class="svg-icon"
style="overflow:hidden;fill:currentColor"
viewBox="0 0 909.87988 546.85529"
version="1.1"
id="svg3"
xml:space="preserve"
width="909.87988"
height="546.85529"
sodipodi:docname="unknown.svg"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.57169944"
inkscape:cx="291.23695"
inkscape:cy="107.57401"
inkscape:window-width="1472"
inkscape:window-height="890"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_7" /><defs
id="defs3"><style
id="style1">.cls-1{fill:#383838;}.cls-2{fill:#9f9f9e;}.cls-3{fill:#cbcccb;}.cls-4{fill:#b7b7b7;}.cls-5{fill:#353535;}.cls-6{fill:#b1a368;}.cls-7{fill:#2c2d2d;}.cls-10,.cls-11,.cls-8,.cls-9{fill:none;stroke:#050606;}.cls-10,.cls-11,.cls-8{stroke-miterlimit:10;}.cls-8,.cls-9{stroke-width:2px;}.cls-9{stroke-linecap:round;stroke-linejoin:round;}.cls-10{stroke-width:2.04px;}.cls-11{stroke-width:1.99px;}.cls-12{fill:#c08c2d;}.cls-13{fill:#af7a2b;}</style></defs><g
id="Layer_7"
data-name="Layer 7"
transform="translate(-646.6554,-758.05941)"><path
class="cls-2"
d="m 1545.1753,893.49468 h 4.69 a 5.67,5.67 0 0 1 5.67,5.67 v 84.64998 a 5.67,5.67 0 0 1 -5.67,5.67 h -4.69"
id="path1-4" /><rect
class="cls-3"
x="647.6554"
y="862.80469"
width="897.52002"
height="441.10999"
rx="11.7"
id="rect2" /><path
class="cls-2"
d="m 681.12532,862.80468 v 113.47998 a 3.67,3.67 0 0 0 3.67,3.67 h 41 a 2.35,2.35 0 0 1 2.35,2.35 V 1303.9147 H 1517.6053 V 862.80468 Z M 1492.6453,1278.9147 H 753.18532 V 972.01466 a 17.06,17.06 0 0 0 -17.06,-17.06 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 h 783.99998 z"
id="path2-7" /><path
class="cls-3"
d="M 1492.6453,887.80468 V 1278.9147 H 753.18532 V 972.01466 a 17,17 0 0 0 -7.2,-13.92 v -70.28998 z"
id="path3-7"
style="fill:#ffffff" /><path
class="cls-4"
d="m 745.98532,887.80468 v 70.28998 a 17,17 0 0 0 -9.86,-3.14 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 z"
id="path4" /><rect
class="cls-2"
x="672.10535"
y="1011.4448"
width="13.53"
height="148.39999"
id="rect4" /><path
class="cls-6"
d="m 1077.2923,853.76468 h 71.71 a 2.55,2.55 0 0 1 2.55,2.55 v 6.48 h -76.8 v -6.48 a 2.55,2.55 0 0 1 2.54,-2.55 z"
id="path7" /><path
class="cls-8"
d="m 1082.9205,761.22647 h 60.8838 a 6.1958134,4.8451518 0 0 1 6.1958,4.84516 v 77.32638 h -73.2754 v -77.32638 a 6.1958134,4.8451518 0 0 1 6.1958,-4.84516 z"
id="path39"
style="fill:#b1a368" /><rect
class="cls-8"
x="1066.9833"
y="778.19855"
width="91.504646"
height="55.957298"
rx="5.5511622"
id="rect39"
style="fill:#b1a368" /><path
class="cls-2"
d="m 1158.4522,782.53954 v 47.24724 a 5.5153484,4.3130254 0 0 1 -5.5512,4.34102 h -80.3665 a 5.5511623,4.341032 0 0 1 -5.587,-4.34102 v -47.24724 a 5.5511623,4.341032 0 0 1 5.587,-4.34103 h 80.5098 a 5.5153484,4.3130254 0 0 1 5.4079,4.34103 z"
id="path41-4"
style="fill:none;stroke:#050606;stroke-width:3.16706;stroke-miterlimit:10" /><rect
class="cls-6"
x="1079.9424"
y="843.73468"
width="65.989998"
height="10.03"
id="rect8" /><path
class="cls-8"
d="M 1492.6453,887.80468 V 1278.9147 H 753.18532 V 972.01466 a 17.06,17.06 0 0 0 -17.06,-17.06 h -27.5 a 2.5,2.5 0 0 1 -2.5,-2.5 v -62.14998 a 2.5,2.5 0 0 1 2.5,-2.5 h 783.99998 m 25,-25 H 681.12532 v 113.47998 a 3.68,3.68 0 0 0 3.67,3.67 h 41 a 2.35,2.35 0 0 1 2.35,2.35 V 1303.9147 H 1517.6053 V 862.80468 Z"
id="path10" /><line
class="cls-8"
x1="745.99536"
y1="958.09467"
x2="745.99536"
y2="887.80469"
id="line10" /><rect
class="cls-8"
x="672.10535"
y="1011.4448"
width="13.53"
height="148.39999"
id="rect11" /><path
class="cls-8"
d="m 1545.1753,893.49468 h 4.69 a 5.67,5.67 0 0 1 5.67,5.67 v 84.64998 a 5.67,5.67 0 0 1 -5.67,5.67 h -4.69"
id="path14" /><path
class="cls-10"
d="m 1077.2923,853.76468 h 71.71 a 2.55,2.55 0 0 1 2.55,2.55 v 6.48 h -76.8 v -6.48 a 2.55,2.55 0 0 1 2.54,-2.55 z"
id="path16" /><rect
class="cls-11"
x="1079.9424"
y="843.73468"
width="65.989998"
height="10.03"
id="rect17" /><path
class="cls-2"
d="m 725.27532,910.38466 a 14,14 0 1 0 14,14 13.95,13.95 0 0 0 -14,-14 z m 0,21.5 a 7.55,7.55 0 1 1 7.54,-7.55 7.55,7.55 0 0 1 -7.54,7.55 z"
id="path19" /><circle
class="cls-8"
cx="725.27539"
cy="924.33466"
r="7.5500002"
id="circle19" /><circle
class="cls-8"
cx="725.27539"
cy="924.33466"
r="13.95"
id="circle20" /><path
d="m 445.36309,440.05365 c 0,11.52004 10.38375,20.85861 23.19309,20.85861 12.80937,0 23.19311,-9.33857 23.19311,-20.85861 0,-11.52005 -10.38374,-20.85861 -23.19311,-20.85861 -12.80934,0 -23.19309,9.33856 -23.19309,20.85861 z"
fill="#ccc"
id="path1"
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.458227"
transform="translate(646.6554,758.05941)" /><path
d="m 469.40305,538.40107 c -119.83415,0 -217.31582,-93.40624 -217.31582,-208.23067 0,-114.82425 97.48167,-208.23058 217.31582,-208.23058 119.83417,0 217.31585,93.40633 217.31585,208.23058 0,114.82443 -97.48168,208.23067 -217.31585,208.23067 z m 0,-386.58065 c -102.63515,0 -186.13149,80.00572 -186.13149,178.34998 0,98.32948 83.49634,178.34997 186.13149,178.34997 102.61966,0 186.13151,-80.01997 186.13151,-178.34997 0,-98.34426 -83.51185,-178.34998 -186.13151,-178.34998 z"
fill="#ccc"
id="path2"
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.474832"
transform="translate(646.6554,758.05941)" /><path
d="m 468.55618,391.96713 c -8.53552,0 -15.46205,-6.22977 -15.46205,-13.90533 v -23.51468 c 0,-22.75028 19.32709,-40.13201 36.39722,-55.47009 12.50833,-11.26363 25.45056,-22.88885 25.45056,-32.16398 0,-23.18095 -20.81195,-42.03718 -46.38573,-42.03718 -26.0067,0 -46.38619,18.0497 -46.38619,41.09158 0,7.67594 -6.92654,13.90533 -15.46208,13.90533 -8.53554,0 -15.46207,-6.22977 -15.46207,-13.9058 0,-37.99002 34.68046,-68.90262 77.31034,-68.90262 42.62989,0 77.31034,31.32967 77.31034,69.84869 0,20.81694 -17.54944,36.5856 -34.51132,51.84064 -13.452,12.07016 -27.33645,24.55758 -27.33645,35.77907 v 23.51468 c 0,7.6764 -6.92702,13.91969 -15.46257,13.91969 z"
fill="#ccc"
id="path3"
style="overflow:hidden;fill:#4d4d4d;stroke-width:0.458227;stroke:#000000;stroke-opacity:1"
transform="translate(646.6554,758.05941)" /><rect
class="cls-8"
x="647.6554"
y="862.80469"
width="897.52002"
height="441.10999"
rx="11.7"
id="rect28" /></g><path
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
id="path11" /><path
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
id="path12" /><path
style="fill:#ffffff;fill-opacity:0;stroke-width:0.92"
d="m 107.41785,363.03448 -0.23786,-156.30546 -2.99,-3.72057 -2.99,-3.72058 v -34.09394 -34.09394 l 150.64998,0.048 150.64999,0.048 -8.28,3.06943 c -19.31509,7.16019 -34.46167,14.82453 -50.21721,25.41044 -50.57644,33.98158 -84.35747,88.86991 -91.06203,147.96009 -1.43336,12.63279 -0.63536,44.7022 1.3876,55.76392 7.76201,42.44321 25.98398,77.92651 55.67763,108.41989 17.37837,17.84644 33.98994,30.29944 55.42867,41.55255 l 11.30534,5.93414 -134.54212,0.0167 -134.54213,0.0167 z"
id="path13" /></svg>

Before

Width:  |  Height:  |  Size: 9.1 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 91 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 120 KiB

View File

File diff suppressed because one or more lines are too long

View File

@@ -35,7 +35,8 @@ data class MyNodeInfo(
val maxChannels: Int,
val hasWifi: Boolean,
val channelUtilization: Float,
val airUtilTx: Float
val airUtilTx: Float,
val deviceId: String?,
) : Parcelable {
/** A human readable description of the software/hardware version */
val firmwareString: String get() = "$model $firmwareVersion"

View File

@@ -129,4 +129,20 @@ class Converters : Logging {
fun metadataToBytes(value: MeshProtos.DeviceMetadata): ByteArray? {
return value.toByteArray()
}
@TypeConverter
fun fromStringList(value: String?): List<String>? {
if (value == null) {
return null
}
return Json.decodeFromString<List<String>>(value)
}
@TypeConverter
fun toStringList(list: List<String>?): String? {
if (list == null) {
return null
}
return Json.encodeToString(list)
}
}

View File

@@ -18,6 +18,8 @@
package com.geeksville.mesh.database
import android.app.Application
import com.geeksville.mesh.database.dao.DeviceHardwareDao
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
import com.geeksville.mesh.database.dao.MeshLogDao
import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.database.dao.PacketDao
@@ -55,4 +57,14 @@ class DatabaseModule {
fun provideQuickChatActionDao(database: MeshtasticDatabase): QuickChatActionDao {
return database.quickChatActionDao()
}
}
@Provides
fun provideDeviceHardwareDao(database: MeshtasticDatabase): DeviceHardwareDao {
return database.deviceHardwareDao()
}
@Provides
fun provideFirmwareReleaseDao(database: MeshtasticDatabase): FirmwareReleaseDao {
return database.firmwareReleaseDao()
}
}

View File

@@ -25,11 +25,15 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.dao.DeviceHardwareDao
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
import com.geeksville.mesh.database.dao.MeshLogDao
import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.dao.QuickChatActionDao
import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.database.entity.MetadataEntity
import com.geeksville.mesh.database.entity.MyNodeEntity
@@ -48,6 +52,8 @@ import com.geeksville.mesh.database.entity.ReactionEntity
QuickChatAction::class,
ReactionEntity::class,
MetadataEntity::class,
DeviceHardwareEntity::class,
FirmwareReleaseEntity::class,
],
autoMigrations = [
AutoMigration(from = 3, to = 4),
@@ -63,8 +69,9 @@ import com.geeksville.mesh.database.entity.ReactionEntity
AutoMigration(from = 13, to = 14),
AutoMigration(from = 14, to = 15),
AutoMigration(from = 15, to = 16),
AutoMigration(from = 16, to = 17),
],
version = 16,
version = 17,
exportSchema = true,
)
@TypeConverters(Converters::class)
@@ -73,6 +80,8 @@ abstract class MeshtasticDatabase : RoomDatabase() {
abstract fun packetDao(): PacketDao
abstract fun meshLogDao(): MeshLogDao
abstract fun quickChatActionDao(): QuickChatActionDao
abstract fun deviceHardwareDao(): DeviceHardwareDao
abstract fun firmwareReleaseDao(): FirmwareReleaseDao
companion object {
fun getDatabase(context: Context): MeshtasticDatabase {
@@ -82,7 +91,7 @@ abstract class MeshtasticDatabase : RoomDatabase() {
MeshtasticDatabase::class.java,
"meshtastic_database"
)
.fallbackToDestructiveMigration()
.fallbackToDestructiveMigration(false)
.build()
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
@Dao
interface DeviceHardwareDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(deviceHardware: DeviceHardwareEntity)
@Query("SELECT * FROM device_hardware WHERE hwModel = :hwModel")
suspend fun getByHwModel(hwModel: Int): DeviceHardwareEntity?
@Query("DELETE FROM device_hardware")
suspend fun deleteAll()
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
import com.geeksville.mesh.database.entity.FirmwareReleaseType
@Dao
interface FirmwareReleaseDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(firmwareReleaseEntity: FirmwareReleaseEntity)
@Query("DELETE FROM firmware_release")
suspend fun deleteAll()
@Query("SELECT * FROM firmware_release")
suspend fun getAllReleases(): List<FirmwareReleaseEntity>?
@Query("SELECT * FROM firmware_release WHERE release_type = :releaseType")
suspend fun getReleasesByType(releaseType: FirmwareReleaseType): List<FirmwareReleaseEntity>?
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.geeksville.mesh.model.DeviceHardware
import com.geeksville.mesh.network.model.NetworkDeviceHardware
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "device_hardware")
data class DeviceHardwareEntity(
@ColumnInfo(name = "actively_supported") val activelySupported: Boolean,
val architecture: String,
@ColumnInfo(name = "display_name") val displayName: String,
@ColumnInfo(name = "has_ink_hud") val hasInkHud: Boolean? = null,
@ColumnInfo(name = "has_mui") val hasMui: Boolean? = null,
@PrimaryKey val hwModel: Int,
@ColumnInfo(name = "hw_model_slug") val hwModelSlug: String,
val images: List<String>?,
@ColumnInfo(name = "last_updated") val lastUpdated: Long = System.currentTimeMillis(),
@ColumnInfo(name = "partition_scheme") val partitionScheme: String? = null,
@ColumnInfo(name = "platformio_target") val platformioTarget: String,
@ColumnInfo(name = "requires_dfu") val requiresDfu: Boolean?,
@ColumnInfo(name = "support_level") val supportLevel: Int?,
val tags: List<String>?,
)
fun NetworkDeviceHardware.asEntity() = DeviceHardwareEntity(
activelySupported = activelySupported,
architecture = architecture,
displayName = displayName,
hasInkHud = hasInkHud,
hasMui = hasMui,
hwModel = hwModel,
hwModelSlug = hwModelSlug,
images = images,
lastUpdated = System.currentTimeMillis(),
partitionScheme = partitionScheme,
platformioTarget = platformioTarget,
requiresDfu = requiresDfu,
supportLevel = supportLevel,
tags = tags,
)
fun DeviceHardwareEntity.asExternalModel() = DeviceHardware(
activelySupported = activelySupported,
architecture = architecture,
displayName = displayName,
hasInkHud = hasInkHud,
hasMui = hasMui,
hwModel = hwModel,
hwModelSlug = hwModelSlug,
images = images,
partitionScheme = partitionScheme,
platformioTarget = platformioTarget,
requiresDfu = requiresDfu,
supportLevel = supportLevel,
tags = tags,
)

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.geeksville.mesh.network.model.NetworkFirmwareRelease
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "firmware_release")
data class FirmwareReleaseEntity(
@PrimaryKey
@ColumnInfo(name = "id")
val id: String = "",
@ColumnInfo(name = "page_url")
val pageUrl: String = "",
@ColumnInfo(name = "release_notes")
val releaseNotes: String = "",
@ColumnInfo(name = "title")
val title: String = "",
@ColumnInfo(name = "zip_url")
val zipUrl: String = "",
@ColumnInfo(name = "last_updated")
val lastUpdated: Long = System.currentTimeMillis(),
@ColumnInfo(name = "release_type")
val releaseType: FirmwareReleaseType = FirmwareReleaseType.STABLE,
)
fun NetworkFirmwareRelease.asEntity(releaseType: FirmwareReleaseType) = FirmwareReleaseEntity(
id = id,
pageUrl = pageUrl,
releaseNotes = releaseNotes,
title = title,
zipUrl = zipUrl,
lastUpdated = System.currentTimeMillis(),
releaseType = releaseType,
)
fun FirmwareReleaseEntity.asExternalModel() = FirmwareRelease(
id = id,
pageUrl = pageUrl,
releaseNotes = releaseNotes,
title = title,
zipUrl = zipUrl,
lastUpdated = lastUpdated,
releaseType = releaseType,
)
data class FirmwareRelease(
val id: String = "",
val pageUrl: String = "",
val releaseNotes: String = "",
val title: String = "",
val zipUrl: String = "",
val lastUpdated: Long = System.currentTimeMillis(),
val releaseType: FirmwareReleaseType = FirmwareReleaseType.STABLE,
)
enum class FirmwareReleaseType {
STABLE,
ALPHA
}

View File

@@ -34,6 +34,7 @@ data class MyNodeEntity(
val minAppVersion: Int,
val maxChannels: Int,
val hasWifi: Boolean,
val deviceId: String? = "unknown",
) {
/** A human readable description of the software/hardware version */
val firmwareString: String get() = "$model $firmwareVersion"
@@ -52,5 +53,6 @@ data class MyNodeEntity(
hasWifi = hasWifi,
channelUtilization = 0f,
airUtilTx = 0f,
deviceId = deviceId,
)
}

View File

@@ -29,7 +29,6 @@ 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
@@ -37,9 +36,12 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.entity.FirmwareRelease
import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.model.map.CustomTileSource
import com.geeksville.mesh.navigation.Route
import com.geeksville.mesh.repository.api.DeviceHardwareRepository
import com.geeksville.mesh.repository.api.FirmwareReleaseRepository
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.service.ServiceAction
import com.geeksville.mesh.ui.map.MAP_STYLE_ID
@@ -56,11 +58,9 @@ 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
@@ -79,6 +79,9 @@ data class MetricsState(
val tracerouteResults: List<MeshPacket> = emptyList(),
val positionLogs: List<Position> = emptyList(),
val deviceHardware: DeviceHardware? = null,
val isLocalDevice: Boolean = false,
val latestStableFirmware: FirmwareRelease? = null,
val latestAlphaFirmware: FirmwareRelease? = null,
) {
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
fun hasSignalMetrics() = signalMetrics.isNotEmpty()
@@ -188,6 +191,7 @@ private fun MeshPacket.toPosition(): Position? = if (!decoded.wantResponse) {
null
}
@Suppress("LongParameterList")
@HiltViewModel
class MetricsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@@ -195,6 +199,8 @@ class MetricsViewModel @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val meshLogRepository: MeshLogRepository,
private val radioConfigRepository: RadioConfigRepository,
private val deviceHardwareRepository: DeviceHardwareRepository,
private val firmwareReleaseRepository: FirmwareReleaseRepository,
private val preferences: SharedPreferences,
) : ViewModel(), Logging {
private val destNum = savedStateHandle.toRoute<Route.NodeDetail>().destNum
@@ -229,25 +235,23 @@ class MetricsViewModel @Inject constructor(
private val _timeFrame = MutableStateFlow(TimeFrame.TWENTY_FOUR_HOURS)
val timeFrame: StateFlow<TimeFrame> = _timeFrame
private var deviceHardwareList: List<DeviceHardware> = listOf()
init {
destNum?.let {
radioConfigRepository.nodeDBbyNum
.mapLatest { nodes -> nodes[destNum] to nodes.keys.firstOrNull() }
.distinctUntilChanged()
.onEach { (node, ourNode) ->
_state.update { state -> state.copy(node = node, isLocal = destNum == ourNode) }
node?.user?.hwModel?.let { hwModel ->
val deviceHardware = getDeviceHardwareFromHardwareModel(hwModel)
deviceHardware?.let {
_state.update { state ->
state.copy(deviceHardware = it)
}
}
val deviceHardware = node?.user?.hwModel?.number?.let {
deviceHardwareRepository.getDeviceHardwareByModel(it)
}
}
.launchIn(viewModelScope)
_state.update { state ->
state.copy(
node = node,
isLocal = destNum == ourNode,
deviceHardware = deviceHardware
)
}
}.launchIn(viewModelScope)
radioConfigRepository.deviceProfileFlow.onEach { profile ->
val moduleConfig = profile.moduleConfig
@@ -308,6 +312,18 @@ class MetricsViewModel @Inject constructor(
}
}.launchIn(viewModelScope)
firmwareReleaseRepository.stableRelease.onEach { latestStable ->
_state.update { state ->
state.copy(latestStableFirmware = latestStable)
}
}.launchIn(viewModelScope)
firmwareReleaseRepository.alphaRelease.onEach { latestAlpha ->
_state.update { state ->
state.copy(latestAlphaFirmware = latestAlpha)
}
}.launchIn(viewModelScope)
debug("MetricsViewModel created")
}
}
@@ -361,22 +377,4 @@ 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<List<DeviceHardware>>(json)
return deviceHardwareList.find { it.hwModel == hwModel.number }
} catch (ex: IOException) {
errormsg("Can't read device_hardware.json error: ${ex.message}")
} catch (ex: IllegalArgumentException) {
errormsg(ex.message.toString())
}
}
return null
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.api
import android.app.Application
import com.geeksville.mesh.network.model.NetworkDeviceHardware
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import javax.inject.Inject
class DeviceHardwareJsonDataSource @Inject constructor(
private val application: Application,
) {
@OptIn(ExperimentalSerializationApi::class)
fun loadDeviceHardwareFromJsonAsset(): List<NetworkDeviceHardware> {
val inputStream = application.assets.open("device_hardware.json")
return Json.decodeFromStream<List<NetworkDeviceHardware>>(inputStream)
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.api
import com.geeksville.mesh.database.dao.DeviceHardwareDao
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
import com.geeksville.mesh.database.entity.asEntity
import com.geeksville.mesh.network.model.NetworkDeviceHardware
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class DeviceHardwareLocalDataSource @Inject constructor(
private val deviceHardwareDaoLazy: dagger.Lazy<DeviceHardwareDao>
) {
private val deviceHardwareDao by lazy {
deviceHardwareDaoLazy.get()
}
suspend fun insertAllDeviceHardware(deviceHardware: List<NetworkDeviceHardware>) =
withContext(Dispatchers.IO) {
deviceHardware.forEach { deviceHardware ->
deviceHardwareDao.insert(deviceHardware.asEntity())
}
}
suspend fun deleteAllDeviceHardware() = withContext(Dispatchers.IO) {
deviceHardwareDao.deleteAll()
}
suspend fun getByHwModel(hwModel: Int): DeviceHardwareEntity? = withContext(Dispatchers.IO) {
deviceHardwareDao.getByHwModel(hwModel)
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.api
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.warn
import com.geeksville.mesh.database.entity.asExternalModel
import com.geeksville.mesh.model.DeviceHardware
import com.geeksville.mesh.network.DeviceHardwareRemoteDataSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import javax.inject.Inject
class DeviceHardwareRepository @Inject constructor(
private val apiDataSource: DeviceHardwareRemoteDataSource,
private val localDataSource: DeviceHardwareLocalDataSource,
private val jsonDataSource: DeviceHardwareJsonDataSource,
) {
companion object {
// 1 day
private const val CACHE_EXPIRATION_TIME_MS = 24 * 60 * 60 * 1000L
}
suspend fun getDeviceHardwareByModel(hwModel: Int, refresh: Boolean = false): DeviceHardware? {
return withContext(Dispatchers.IO) {
if (refresh) {
invalidateCache()
} else {
val cachedHardware = localDataSource.getByHwModel(hwModel)
if (cachedHardware != null && !isCacheExpired(cachedHardware.lastUpdated)) {
debug("Using recent cached device hardware")
val externalModel = cachedHardware.asExternalModel()
return@withContext externalModel
}
}
try {
debug("Fetching device hardware from server")
localDataSource.insertAllDeviceHardware(apiDataSource.getAllDeviceHardware())
val cachedHardware = localDataSource.getByHwModel(hwModel)
val externalModel = cachedHardware?.asExternalModel()
return@withContext externalModel
} catch (e: IOException) {
warn("Failed to fetch device hardware from server: ${e.message}")
var cachedHardware = localDataSource.getByHwModel(hwModel)
if (cachedHardware != null) {
debug("Using stale cached device hardware")
return@withContext cachedHardware.asExternalModel()
}
debug("Loading and caching device hardware from local JSON asset")
localDataSource.insertAllDeviceHardware(jsonDataSource.loadDeviceHardwareFromJsonAsset())
cachedHardware = localDataSource.getByHwModel(hwModel)
val externalModel = cachedHardware?.asExternalModel()
return@withContext externalModel
}
}
}
suspend fun invalidateCache() {
localDataSource.deleteAllDeviceHardware()
}
/**
* Check if the cache is expired
*/
private fun isCacheExpired(lastUpdated: Long): Boolean {
return System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.api
import android.app.Application
import com.geeksville.mesh.network.model.NetworkFirmwareReleases
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import javax.inject.Inject
class FirmwareReleaseJsonDataSource @Inject constructor(
private val application: Application,
) {
@OptIn(ExperimentalSerializationApi::class)
fun loadFirmwareReleaseFromJsonAsset(): NetworkFirmwareReleases {
val inputStream = application.assets.open("firmware_releases.json")
val result = inputStream.use {
Json.decodeFromStream<NetworkFirmwareReleases>(inputStream)
}
return result
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.api
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
import com.geeksville.mesh.database.entity.FirmwareReleaseType
import com.geeksville.mesh.database.entity.asEntity
import com.geeksville.mesh.network.model.NetworkFirmwareRelease
import dagger.Lazy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class FirmwareReleaseLocalDataSource @Inject constructor(
private val firmwareReleaseDaoLazy: Lazy<FirmwareReleaseDao>
) {
private val firmwareReleaseDao by lazy {
firmwareReleaseDaoLazy.get()
}
suspend fun insertFirmwareReleases(
firmwareReleases: List<NetworkFirmwareRelease>,
releaseType: FirmwareReleaseType
) =
withContext(Dispatchers.IO) {
firmwareReleases.forEach { firmwareRelease ->
firmwareReleaseDao.insert(firmwareRelease.asEntity(releaseType))
}
}
suspend fun deleteAllFirmwareReleases() = withContext(Dispatchers.IO) {
firmwareReleaseDao.deleteAll()
}
suspend fun getLatestRelease(releaseType: FirmwareReleaseType): FirmwareReleaseEntity? =
withContext(Dispatchers.IO) {
val releases = firmwareReleaseDao.getReleasesByType(releaseType)
val latestRelease =
releases?.firstOrNull()
return@withContext latestRelease
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.repository.api
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.warn
import com.geeksville.mesh.database.entity.FirmwareRelease
import com.geeksville.mesh.database.entity.FirmwareReleaseType
import com.geeksville.mesh.database.entity.asExternalModel
import com.geeksville.mesh.network.FirmwareReleaseRemoteDataSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.IOException
import javax.inject.Inject
class FirmwareReleaseRepository @Inject constructor(
private val apiDataSource: FirmwareReleaseRemoteDataSource,
private val localDataSource: FirmwareReleaseLocalDataSource,
private val jsonDataSource: FirmwareReleaseJsonDataSource,
) {
companion object {
// 1 hour
private const val CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000L
}
val stableRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.STABLE)
val alphaRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.ALPHA)
private fun getLatestFirmware(
releaseType: FirmwareReleaseType,
refresh: Boolean = false
): Flow<FirmwareRelease?> = flow {
if (refresh) {
invalidateCache()
} else {
val cachedRelease = localDataSource.getLatestRelease(releaseType)
if (cachedRelease != null && !isCacheExpired(cachedRelease.lastUpdated)) {
debug("Using recent cached firmware release")
val externalModel = cachedRelease.asExternalModel()
emit(externalModel)
return@flow
}
}
try {
debug("Fetching firmware releases from server")
val networkFirmwareReleases = apiDataSource.getFirmwareReleases()
val releases = when (releaseType) {
FirmwareReleaseType.STABLE -> networkFirmwareReleases.releases.stable
FirmwareReleaseType.ALPHA -> networkFirmwareReleases.releases.alpha
}
localDataSource.insertFirmwareReleases(
releases,
releaseType
)
val cachedRelease = localDataSource.getLatestRelease(releaseType)
val externalModel = cachedRelease?.asExternalModel()
emit(externalModel)
} catch (e: IOException) {
warn("Failed to fetch firmware releases from server: ${e.message}")
val jsonFirmwareReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
val releases = when (releaseType) {
FirmwareReleaseType.STABLE -> jsonFirmwareReleases.releases.stable
FirmwareReleaseType.ALPHA -> jsonFirmwareReleases.releases.alpha
}
localDataSource.insertFirmwareReleases(
releases,
releaseType
)
val cachedRelease = localDataSource.getLatestRelease(releaseType)
val externalModel = cachedRelease?.asExternalModel()
emit(externalModel)
}
}
suspend fun invalidateCache() {
localDataSource.deleteAllFirmwareReleases()
}
/**
* Check if the cache is expired
*/
private fun isCacheExpired(lastUpdated: Long): Boolean {
return System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
}
}

View File

@@ -1604,6 +1604,7 @@ class MeshService : Service(), Logging {
minAppVersion = minAppVersion,
maxChannels = 8,
hasWifi = metadata.hasWifi,
deviceId = deviceId.toStringUtf8(),
)
}
serviceScope.handledLaunch {

View File

@@ -57,11 +57,11 @@ import androidx.compose.material.icons.filled.Share
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
import androidx.compose.material.icons.outlined.NoCell
import androidx.compose.material.icons.twotone.Verified
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
@@ -79,6 +79,7 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
@@ -90,7 +91,11 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import com.geeksville.mesh.R
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.model.DeviceHardware
import com.geeksville.mesh.model.MetricsState
import com.geeksville.mesh.model.MetricsViewModel
@@ -207,6 +212,33 @@ private fun NodeDetailList(
NodeDetailsContent(node)
}
}
node.metadata?.firmwareVersion?.let { firmwareVersion ->
item {
PreferenceCategory(stringResource(R.string.firmware)) {
val latestStableFirmware = metricsState.latestStableFirmware
val latestAlphaFirmware = metricsState.latestAlphaFirmware
NodeDetailRow(
label = "Installed",
icon = Icons.Default.Memory,
value = firmwareVersion.substringBeforeLast(".")
)
latestStableFirmware?.let { stable ->
NodeDetailRow(
label = "Latest stable",
icon = Icons.Default.Memory,
value = stable.id.substringBeforeLast(".").replace("v", "")
)
}
latestAlphaFirmware?.let { alpha ->
NodeDetailRow(
label = "Latest alpha",
icon = Icons.Default.Memory,
value = alpha.id.substringBeforeLast(".").replace("v", "")
)
}
}
}
}
item {
DeviceActions(
@@ -332,25 +364,19 @@ private fun DeviceDetailsContent(
),
contentAlignment = Alignment.Center
) {
DeviceHardwareImage(
deviceHardware = deviceHardware,
modifier = Modifier
.size(100.dp)
)
DeviceHardwareImage(deviceHardware, Modifier.fillMaxSize())
}
NodeDetailRow(
label = stringResource(R.string.hardware),
icon = Icons.Default.Router,
value = hwModelName
)
if (isSupported) {
NodeDetailRow(
label = stringResource(R.string.supported),
icon = Icons.Default.Verified,
value = "",
iconTint = Color.Green
)
}
NodeDetailRow(
label = if (isSupported) stringResource(R.string.supported) else "Supported by Community",
icon = if (isSupported) Icons.TwoTone.Verified else ImageVector.vectorResource(R.drawable.unverified),
value = "",
iconTint = if (isSupported) Color.Green else Color.Red
)
}
@Composable
@@ -358,20 +384,37 @@ fun DeviceHardwareImage(
deviceHardware: DeviceHardware,
modifier: Modifier = Modifier,
) {
val hwImg = deviceHardware.images?.lastOrNull()
if (hwImg != null) {
val imageUrl = "file:///android_asset/device_hardware/$hwImg"
AsyncImage(
model = imageUrl,
contentScale = ContentScale.Inside,
contentDescription = deviceHardware.displayName,
placeholder = painterResource(R.drawable.hw_unknown),
error = painterResource(R.drawable.hw_unknown),
fallback = painterResource(R.drawable.hw_unknown),
modifier = modifier
.padding(16.dp)
)
val hwImg = deviceHardware.images?.get(1) ?: deviceHardware.images?.get(0) ?: "unknown.svg"
val imageUrl = "https://flasher.meshtastic.org/img/devices/$hwImg"
val listener = object : ImageRequest.Listener {
override fun onStart(request: ImageRequest) {
super.onStart(request)
debug("Image request started")
}
override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result)
debug("Image request failed: ${result.throwable.message}")
}
override fun onSuccess(request: ImageRequest, result: SuccessResult) {
super.onSuccess(request, result)
debug("Image request succeeded: ${result.dataSource.name}")
}
}
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.listener(listener)
.data(imageUrl)
.build(),
contentScale = ContentScale.Inside,
contentDescription = deviceHardware.displayName,
placeholder = painterResource(R.drawable.hw_unknown),
error = painterResource(R.drawable.hw_unknown),
fallback = painterResource(R.drawable.hw_unknown),
modifier = modifier
.padding(16.dp)
)
}
@Suppress("LongMethod")
@@ -435,13 +478,6 @@ private fun NodeDetailsContent(
value = formatUptime(node.deviceMetrics.uptimeSeconds)
)
}
if (node.metadata != null) {
NodeDetailRow(
label = stringResource(R.string.firmware_version),
icon = Icons.Default.Memory,
value = node.metadata.firmwareVersion.substringBeforeLast(".")
)
}
NodeDetailRow(
label = stringResource(R.string.node_sort_last_heard),
icon = Icons.Default.History,

View File

@@ -1,138 +1,121 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="909.9dp"
android:height="1044.4dp"
android:viewportWidth="909.9"
android:viewportHeight="1044.4">
android:width="909.88dp"
android:height="546.86dp"
android:viewportWidth="909.88"
android:viewportHeight="546.86">
<path
android:pathData="m898.5,633h4.7a5.7,5.7 0,0 1,5.7 5.7v84.6a5.7,5.7 0,0 1,-5.7 5.7h-4.7"
android:pathData="m898.52,135.44h4.69a5.67,5.67 0,0 1,5.67 5.67v84.65a5.67,5.67 0,0 1,-5.67 5.67h-4.69"
android:fillColor="#9f9f9e"/>
<path
android:pathData="M12.7,602.3L886.8,602.3A11.7,11.7 0,0 1,898.5 614L898.5,1031.7A11.7,11.7 0,0 1,886.8 1043.4L12.7,1043.4A11.7,11.7 0,0 1,1 1031.7L1,614A11.7,11.7 0,0 1,12.7 602.3z"
android:pathData="M12.7,104.75L886.82,104.75A11.7,11.7 0,0 1,898.52 116.45L898.52,534.16A11.7,11.7 0,0 1,886.82 545.86L12.7,545.86A11.7,11.7 0,0 1,1 534.16L1,116.45A11.7,11.7 0,0 1,12.7 104.75z"
android:fillColor="#cbcccb"/>
<path
android:pathData="m34.5,602.3v113.5a3.7,3.7 0,0 0,3.7 3.7h41a2.3,2.3 0,0 1,2.3 2.3L81.5,1043.4L870.9,1043.4L870.9,602.3ZM846,1018.4L106.5,1018.4L106.5,711.5a17.1,17.1 0,0 0,-17.1 -17.1h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.1a2.5,2.5 0,0 1,2.5 -2.5h784z"
android:pathData="m34.47,104.75v113.48a3.67,3.67 0,0 0,3.67 3.67h41a2.35,2.35 0,0 1,2.35 2.35L81.49,545.86L870.95,545.86L870.95,104.75ZM845.99,520.86L106.53,520.86L106.53,213.96a17.06,17.06 0,0 0,-17.06 -17.06h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.15a2.5,2.5 0,0 1,2.5 -2.5h784z"
android:fillColor="#9f9f9e"/>
<path
android:pathData="M846,627.3L846,1018.4L106.5,1018.4L106.5,711.5a17,17 0,0 0,-7.2 -13.9v-70.3z"
android:pathData="M845.99,129.75L845.99,520.86L106.53,520.86L106.53,213.96a17,17 0,0 0,-7.2 -13.92v-70.29z"
android:fillColor="#cbcccb"/>
<path
android:pathData="m99.3,627.3v70.3a17,17 0,0 0,-9.9 -3.1h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.1a2.5,2.5 0,0 1,2.5 -2.5z"
android:pathData="m99.33,129.75v70.29a17,17 0,0 0,-9.86 -3.14h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.15a2.5,2.5 0,0 1,2.5 -2.5z"
android:fillColor="#b7b7b7"/>
<path
android:pathData="M25.4,750.9h13.5v148.4h-13.5z"
android:pathData="M25.45,253.39h13.53v148.4h-13.53z"
android:fillColor="#9f9f9e"/>
<path
android:pathData="M417.9,487.9L513.9,487.9A3.9,3.9 0,0 1,517.9 491.8L517.9,548.6A3.9,3.9 0,0 1,513.9 552.5L417.9,552.5A3.9,3.9 0,0 1,413.9 548.6L413.9,491.8A3.9,3.9 0,0 1,417.9 487.9z"
android:pathData="m430.64,95.71h71.71a2.55,2.55 0,0 1,2.55 2.55v6.48h-76.8v-6.48a2.55,2.55 0,0 1,2.54 -2.55z"
android:fillColor="#b1a368"/>
<path
android:pathData="m430.6,593.2h71.7a2.5,2.5 0,0 1,2.5 2.5v6.5h-76.8v-6.5a2.5,2.5 0,0 1,2.5 -2.5z"
android:fillColor="#b1a368"/>
<path
android:pathData="m419.2,552.5h92.4v28.1a2.5,2.5 0,0 1,-2.5 2.5h-87.3a2.5,2.5 0,0 1,-2.5 -2.5v-28.1z"
android:fillColor="#b1a368"/>
<path
android:pathData="M433.3,583.2h66v10h-66z"
android:fillColor="#b1a368"/>
<path
android:pathData="M507.2,487.9l0,-4.3l-82.7,0l0,4.3"
android:fillColor="#b1a368"/>
<path
android:pathData="m465.9,1v0a44,44 0,0 1,44 44v438.6h-88v-438.5a44,44 0,0 1,44 -44z"
android:fillColor="#383838"/>
<path
android:pathData="M421.9,65.7h88v6h-88z"
android:fillColor="#2c2d2d"/>
<path
android:pathData="M421.9,92.9h88v6h-88z"
android:fillColor="#2c2d2d"/>
<path
android:pathData="M846,627.3L846,1018.4L106.5,1018.4L106.5,711.5a17.1,17.1 0,0 0,-17.1 -17.1h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.1a2.5,2.5 0,0 1,2.5 -2.5h784m25,-25L34.5,602.3v113.5a3.7,3.7 0,0 0,3.7 3.7h41a2.3,2.3 0,0 1,2.3 2.3L81.5,1043.4L870.9,1043.4L870.9,602.3Z"
android:pathData="m436.27,3.17h60.88a6.2,4.85 0,0 1,6.2 4.85v77.33h-73.28v-77.33a6.2,4.85 0,0 1,6.2 -4.85z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="M99.3,697.5L99.3,627.3"
android:pathData="M425.88,20.14L506.28,20.14A5.55,5.55 0,0 1,511.83 25.69L511.83,70.55A5.55,5.55 0,0 1,506.28 76.1L425.88,76.1A5.55,5.55 0,0 1,420.33 70.55L420.33,25.69A5.55,5.55 0,0 1,425.88 20.14z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="M25.4,750.9h13.5v148.4h-13.5z"
android:pathData="m511.8,24.48v47.25a5.52,4.31 0,0 1,-5.55 4.34h-80.37a5.55,4.34 0,0 1,-5.59 -4.34v-47.25a5.55,4.34 0,0 1,5.59 -4.34h80.51a5.52,4.31 0,0 1,5.41 4.34z"
android:strokeWidth="3.16706"
android:fillColor="#9f9f9e"
android:strokeColor="#050606"/>
<path
android:pathData="M433.29,85.68h65.99v10.03h-65.99z"
android:fillColor="#b1a368"/>
<path
android:pathData="M845.99,129.75L845.99,520.86L106.53,520.86L106.53,213.96a17.06,17.06 0,0 0,-17.06 -17.06h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.15a2.5,2.5 0,0 1,2.5 -2.5h784m25,-25L34.47,104.75v113.48a3.68,3.68 0,0 0,3.67 3.67h41a2.35,2.35 0,0 1,2.35 2.35L81.49,545.86L870.95,545.86L870.95,104.75Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="m898.5,633h4.7a5.7,5.7 0,0 1,5.7 5.7v84.6a5.7,5.7 0,0 1,-5.7 5.7h-4.7"
android:pathData="M99.34,200.04L99.34,129.75"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="m465.9,1v0a44,44 0,0 1,44 44v438.6h-88v-438.5a44,44 0,0 1,44 -44z"
android:pathData="M25.45,253.39h13.53v148.4h-13.53z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="M417.9,487.9L513.9,487.9A3.9,3.9 0,0 1,517.9 491.8L517.9,548.6A3.9,3.9 0,0 1,513.9 552.5L417.9,552.5A3.9,3.9 0,0 1,413.9 548.6L413.9,491.8A3.9,3.9 0,0 1,417.9 487.9z"
android:pathData="m898.52,135.44h4.69a5.67,5.67 0,0 1,5.67 5.67v84.65a5.67,5.67 0,0 1,-5.67 5.67h-4.69"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="m430.64,95.71h71.71a2.55,2.55 0,0 1,2.55 2.55v6.48h-76.8v-6.48a2.55,2.55 0,0 1,2.54 -2.55z"
android:strokeWidth="2.04"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="m430.6,593.2h71.7a2.5,2.5 0,0 1,2.5 2.5v6.5h-76.8v-6.5a2.5,2.5 0,0 1,2.5 -2.5z"
android:strokeWidth="2.04"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="m419.2,552.5h92.4v28.1a2.5,2.5 0,0 1,-2.5 2.5h-87.3a2.5,2.5 0,0 1,-2.5 -2.5v-28.1z"
android:strokeWidth="2.04"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="M433.3,583.2h66v10h-66z"
android:pathData="M433.29,85.68h65.99v10.03h-65.99z"
android:strokeWidth="1.99"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="M507.2,487.9l0,-4.3l-82.7,0l0,4.3"
android:strokeWidth="2.04"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="M421.9,65.7h88v6h-88z"
android:strokeWidth="2.04"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="M421.9,92.9h88v6h-88z"
android:strokeWidth="2.04"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="m78.6,649.8a14,14 0,1 0,14 14,13.9 13.9,0 0,0 -14,-14zM78.6,671.3a7.6,7.6 0,1 1,7.5 -7.6,7.6 7.6,0 0,1 -7.5,7.6z"
android:pathData="m78.62,152.33a14,14 0,1 0,14 14,13.95 13.95,0 0,0 -14,-14zM78.62,173.83a7.55,7.55 0,1 1,7.54 -7.55,7.55 7.55,0 0,1 -7.54,7.55z"
android:fillColor="#9f9f9e"/>
<path
android:pathData="M78.6,663.8m-7.6,0a7.6,7.6 0,1 1,15.1 0a7.6,7.6 0,1 1,-15.1 0"
android:pathData="M78.62,166.28m-7.55,0a7.55,7.55 0,1 1,15.1 0a7.55,7.55 0,1 1,-15.1 0"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="M78.6,663.8m-13.9,0a13.9,13.9 0,1 1,27.9 0a13.9,13.9 0,1 1,-27.9 0"
android:pathData="M78.62,166.28m-13.95,0a13.95,13.95 0,1 1,27.9 0a13.95,13.95 0,1 1,-27.9 0"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="M12.7,602.3L886.8,602.3A11.7,11.7 0,0 1,898.5 614L898.5,1031.7A11.7,11.7 0,0 1,886.8 1043.4L12.7,1043.4A11.7,11.7 0,0 1,1 1031.7L1,614A11.7,11.7 0,0 1,12.7 602.3z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="m445.4,937.6c0,11.5 10.4,20.9 23.2,20.9 12.8,0 23.2,-9.3 23.2,-20.9 0,-11.5 -10.4,-20.9 -23.2,-20.9 -12.8,0 -23.2,9.3 -23.2,20.9z"
android:pathData="m445.36,440.05c0,11.52 10.38,20.86 23.19,20.86 12.81,0 23.19,-9.34 23.19,-20.86 0,-11.52 -10.38,-20.86 -23.19,-20.86 -12.81,0 -23.19,9.34 -23.19,20.86z"
android:strokeWidth="0.458227"
android:fillColor="#4d4d4d"/>
<path
android:pathData="m469.4,1035.9c-119.8,0 -217.3,-93.4 -217.3,-208.2 0,-114.8 97.5,-208.2 217.3,-208.2 119.8,0 217.3,93.4 217.3,208.2 0,114.8 -97.5,208.2 -217.3,208.2zM469.4,649.3c-102.6,0 -186.1,80 -186.1,178.3 0,98.3 83.5,178.3 186.1,178.3 102.6,0 186.1,-80 186.1,-178.3 0,-98.3 -83.5,-178.3 -186.1,-178.3z"
android:pathData="m469.4,538.4c-119.83,0 -217.32,-93.41 -217.32,-208.23 0,-114.82 97.48,-208.23 217.32,-208.23 119.83,0 217.32,93.41 217.32,208.23 0,114.82 -97.48,208.23 -217.32,208.23zM469.4,151.82c-102.64,0 -186.13,80.01 -186.13,178.35 0,98.33 83.5,178.35 186.13,178.35 102.62,0 186.13,-80.02 186.13,-178.35 0,-98.34 -83.51,-178.35 -186.13,-178.35z"
android:strokeWidth="0.474832"
android:fillColor="#4d4d4d"/>
<path
android:pathData="m468.6,889.5c-8.5,0 -15.5,-6.2 -15.5,-13.9v-23.5c0,-22.8 19.3,-40.1 36.4,-55.5 12.5,-11.3 25.5,-22.9 25.5,-32.2 0,-23.2 -20.8,-42 -46.4,-42 -26,0 -46.4,18 -46.4,41.1 0,7.7 -6.9,13.9 -15.5,13.9 -8.5,0 -15.5,-6.2 -15.5,-13.9 0,-38 34.7,-68.9 77.3,-68.9 42.6,0 77.3,31.3 77.3,69.8 0,20.8 -17.5,36.6 -34.5,51.8 -13.5,12.1 -27.3,24.6 -27.3,35.8v23.5c0,7.7 -6.9,13.9 -15.5,13.9z"
android:pathData="m468.56,391.97c-8.54,0 -15.46,-6.23 -15.46,-13.91v-23.51c0,-22.75 19.33,-40.13 36.4,-55.47 12.51,-11.26 25.45,-22.89 25.45,-32.16 0,-23.18 -20.81,-42.04 -46.39,-42.04 -26.01,0 -46.39,18.05 -46.39,41.09 0,7.68 -6.93,13.91 -15.46,13.91 -8.54,0 -15.46,-6.23 -15.46,-13.91 0,-37.99 34.68,-68.9 77.31,-68.9 42.63,0 77.31,31.33 77.31,69.85 0,20.82 -17.55,36.59 -34.51,51.84 -13.45,12.07 -27.34,24.56 -27.34,35.78v23.51c0,7.68 -6.93,13.92 -15.46,13.92z"
android:strokeWidth="0.458227"
android:fillColor="#4d4d4d"/>
android:fillColor="#4d4d4d"
android:strokeColor="#000000"/>
<path
android:pathData="M12.7,104.75L886.82,104.75A11.7,11.7 0,0 1,898.52 116.45L898.52,534.16A11.7,11.7 0,0 1,886.82 545.86L12.7,545.86A11.7,11.7 0,0 1,1 534.16L1,116.45A11.7,11.7 0,0 1,12.7 104.75z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#050606"/>
<path
android:pathData="m107.42,363.03 l-0.24,-156.31 -2.99,-3.72 -2.99,-3.72v-34.09,-34.09l150.65,0.05 150.65,0.05 -8.28,3.07c-19.32,7.16 -34.46,14.82 -50.22,25.41 -50.58,33.98 -84.36,88.87 -91.06,147.96 -1.43,12.63 -0.64,44.7 1.39,55.76 7.76,42.44 25.98,77.93 55.68,108.42 17.38,17.85 33.99,30.3 55.43,41.55l11.31,5.93 -134.54,0.02 -134.54,0.02z"
android:strokeWidth="0.92"
android:fillColor="#ffffff"
android:fillAlpha="0"/>
<path
android:pathData="m107.42,363.03 l-0.24,-156.31 -2.99,-3.72 -2.99,-3.72v-34.09,-34.09l150.65,0.05 150.65,0.05 -8.28,3.07c-19.32,7.16 -34.46,14.82 -50.22,25.41 -50.58,33.98 -84.36,88.87 -91.06,147.96 -1.43,12.63 -0.64,44.7 1.39,55.76 7.76,42.44 25.98,77.93 55.68,108.42 17.38,17.85 33.99,30.3 55.43,41.55l11.31,5.93 -134.54,0.02 -134.54,0.02z"
android:strokeWidth="0.92"
android:fillColor="#ffffff"
android:fillAlpha="0"/>
<path
android:pathData="m107.42,363.03 l-0.24,-156.31 -2.99,-3.72 -2.99,-3.72v-34.09,-34.09l150.65,0.05 150.65,0.05 -8.28,3.07c-19.32,7.16 -34.46,14.82 -50.22,25.41 -50.58,33.98 -84.36,88.87 -91.06,147.96 -1.43,12.63 -0.64,44.7 1.39,55.76 7.76,42.44 25.98,77.93 55.68,108.42 17.38,17.85 33.99,30.3 55.43,41.55l11.31,5.93 -134.54,0.02 -134.54,0.02z"
android:strokeWidth="0.92"
android:fillColor="#ffffff"
android:fillAlpha="0"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M23,11.99l-2.44,-2.79l0.34,-3.69l-3.61,-0.82L15.4,1.5L12,2.96L8.6,1.5L6.71,4.69L3.1,5.5L3.44,9.2L1,11.99l2.44,2.79l-0.34,3.7l3.61,0.82L8.6,22.5l3.4,-1.47l3.4,1.46l1.89,-3.19l3.61,-0.82l-0.34,-3.69L23,11.99zM19.05,13.47l-0.56,0.65l0.08,0.85l0.18,1.95l-1.9,0.43l-0.84,0.19l-0.44,0.74l-0.99,1.68l-1.78,-0.77L12,18.85l-0.79,0.34l-1.78,0.77l-0.99,-1.67l-0.44,-0.74l-0.84,-0.19l-1.9,-0.43l0.18,-1.96l0.08,-0.85l-0.56,-0.65l-1.29,-1.47l1.29,-1.48l0.56,-0.65L5.43,9.01L5.25,7.07l1.9,-0.43l0.84,-0.19l0.44,-0.74l0.99,-1.68l1.78,0.77L12,5.14l0.79,-0.34l1.78,-0.77l0.99,1.68l0.44,0.74l0.84,0.19l1.9,0.43l-0.18,1.95l-0.08,0.85l0.56,0.65l1.29,1.47L19.05,13.47z"
android:fillColor="#e3e3e3"/>
</vector>

View File

@@ -628,4 +628,5 @@
<string name="import_label">Import</string>
<string name="request_metadata">Request Metadata</string>
<string name="actions">Actions</string>
<string name="firmware">Firmware</string>
</resources>

View File

@@ -33,11 +33,13 @@ material = "1.12.0"
material3 = "1.3.2"
mgrs = "2.1.3"
navigation = "2.9.0"
okhttp = "4.12.0"
org-eclipse-paho-client-mqttv3 = "1.2.5"
osmbonuspack = "6.9.0"
osmdroid-android = "6.1.14"
protobuf-gradle-plugin = "0.9.5"
protobuf-kotlin = "4.31.0"
retrofit = "2.11.0"
room = "2.7.1"
streamsupport-minifuture = "1.7.4"
usb-serial-android = "3.9.0"
@@ -57,6 +59,8 @@ appintro = { group = "com.github.AppIntro", name = "AppIntro", version.ref = "ap
awesome-app-rating = { group = "com.suddenh4x.ratingdialog", name = "awesome-app-rating", version.ref = "awesome-app-rating" }
cardview = { group = "androidx.cardview", name = "cardview", version.ref = "cardview" }
coil = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
coil-network-core = { group = "io.coil-kt.coil3", name = "coil-network-core", version.ref = "coil" }
coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" }
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
@@ -106,6 +110,8 @@ material = { group = "com.google.android.material", name = "material", version.r
mgrs = { group = "mil.nga", name = "mgrs", version.ref = "mgrs" }
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "navigation" }
okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
org-eclipse-paho-client-mqttv3 = { group = "org.eclipse.paho", name = "org.eclipse.paho.client.mqttv3", version.ref = "org-eclipse-paho-client-mqttv3" }
osmbonuspack = { group = "com.github.MKergall", name = "osmbonuspack", version.ref = "osmbonuspack" }
osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version.ref = "osmdroid-android" }
@@ -114,6 +120,8 @@ osmdroid-wms = { group = "org.osmdroid", name = "osmdroid-wms", version.ref = "o
protobuf-gradle-plugin = { group = "com.google.protobuf", name = "protobuf-gradle-plugin", version.ref = "protobuf-gradle-plugin" }
protobuf-kotlin = { group = "com.google.protobuf", name = "protobuf-kotlin", version.ref = "protobuf-kotlin" }
protobuf-protoc = { group = "com.google.protobuf", name ="protoc", version.ref = "protobuf-kotlin" }
retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit2-kotlin-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
@@ -163,11 +171,14 @@ osm = ["osmdroid-android", "osmdroid-wms", "osmbonuspack", "mgrs"]
# Firebase
firebase = ["firebase-analytics", "firebase-crashlytics"]
#Protobuf
# Protobuf
protobuf = ["protobuf-kotlin"]
# retrofit
retrofit = ["retrofit2", "retrofit2-kotlin-serialization", "okhttp3", "okhttp3-logging-interceptor"]
# coil
coil = ["coil", "coil-svg"]
coil = ["coil", "coil-network-core", "coil-network-okhttp", "coil-svg"]
[plugins]
android-application = { id = "com.android.application" }
@@ -178,6 +189,7 @@ hilt = { id = "com.google.dagger.hilt.android" }
kotlin-android = { id = "org.jetbrains.kotlin.android" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize" }
kotlin-serialization = { id = "kotlinx-serialization" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization" }
protobuf = { id = "com.google.protobuf" }
android-library = { id = "com.android.library" }

1
network/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

46
network/build.gradle.kts Normal file
View File

@@ -0,0 +1,46 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt)
alias(libs.plugins.devtools.ksp)
alias(libs.plugins.detekt)
id("kotlinx-serialization")
}
android {
buildFeatures {
buildConfig = true
}
compileSdk = 35
defaultConfig {
minSdk = 21
}
namespace = "com.geeksville.mesh.network"
compileOptions {
sourceCompatibility(JavaVersion.VERSION_17)
targetCompatibility(JavaVersion.VERSION_17)
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
dependencies {
implementation(libs.bundles.hilt)
implementation(libs.bundles.retrofit)
implementation(libs.bundles.coil)
ksp(libs.hilt.compiler)
implementation(libs.kotlinx.serialization.json)
detektPlugins(libs.detekt.formatting)
}
detekt {
config.setFrom("../config/detekt/detekt.yml")
baseline = file("../config/detekt/detekt-baseline.xml")
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.network
import com.geeksville.mesh.network.model.NetworkDeviceHardware
import com.geeksville.mesh.network.retrofit.ApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class DeviceHardwareRemoteDataSource @Inject constructor(
private val apiService: ApiService,
) {
suspend fun getAllDeviceHardware(): List<NetworkDeviceHardware> = withContext(Dispatchers.IO) {
apiService.getDeviceHardware().body() ?: emptyList()
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.network
import com.geeksville.mesh.network.model.NetworkFirmwareReleases
import com.geeksville.mesh.network.retrofit.ApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class FirmwareReleaseRemoteDataSource @Inject constructor(
private val apiService: ApiService,
) {
suspend fun getFirmwareReleases(): NetworkFirmwareReleases = withContext(Dispatchers.IO) {
apiService.getFirmwareReleases().body() ?: NetworkFirmwareReleases()
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.network.di
import android.content.Context
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.crossfade
import coil3.svg.SvgDecoder
import coil3.util.DebugLogger
import coil3.util.Logger
import com.geeksville.mesh.network.BuildConfig
import com.geeksville.mesh.network.retrofit.ApiService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import javax.inject.Singleton
private const val DISK_CACHE_PERCENT = 0.02
private const val MEMORY_CACHE_PERCENT = 0.25
@InstallIn(SingletonComponent::class)
@Module
class ApiModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
val loggingInterceptor = HttpLoggingInterceptor().apply {
if (BuildConfig.DEBUG) {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
}
return OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
}
@Provides
@Singleton
fun provideRetrofit(
okHttpClient: OkHttpClient
): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.meshtastic.org/") // Replace with your base URL
.addConverterFactory(
Json.asConverterFactory(
"application/json; charset=UTF8".toMediaType()
)
)
.client(okHttpClient)
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
@Provides
@Singleton
fun imageLoader(
httpClient: OkHttpClient,
@ApplicationContext application: Context,
): ImageLoader {
val sharedOkHttp = httpClient.newBuilder().build()
return ImageLoader.Builder(application)
.components {
add(
OkHttpNetworkFetcherFactory({ sharedOkHttp })
)
add(SvgDecoder.Factory())
}
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(application, MEMORY_CACHE_PERCENT)
.build()
}
.diskCache {
DiskCache.Builder()
.maxSizePercent(DISK_CACHE_PERCENT)
.build()
}
.logger(if (BuildConfig.DEBUG) DebugLogger(Logger.Level.Verbose) else null)
.crossfade(true)
.build()
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class NetworkDeviceHardware(
@SerialName("activelySupported")
val activelySupported: Boolean = false,
@SerialName("architecture")
val architecture: String = "",
@SerialName("displayName")
val displayName: String = "",
@SerialName("hasInkHud")
val hasInkHud: Boolean? = null,
@SerialName("hasMui")
val hasMui: Boolean? = null,
@SerialName("hwModel")
val hwModel: Int = 0,
@SerialName("hwModelSlug")
val hwModelSlug: String = "",
@SerialName("images")
val images: List<String>? = null,
@SerialName("partitionScheme")
val partitionScheme: String? = null,
@SerialName("platformioTarget")
val platformioTarget: String = "",
@SerialName("requiresDfu")
val requiresDfu: Boolean? = null,
@SerialName("supportLevel")
val supportLevel: Int? = null,
@SerialName("tags")
val tags: List<String>? = null
)

View File

@@ -0,0 +1,51 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class NetworkFirmwareRelease(
@SerialName("id")
val id: String = "",
@SerialName("page_url")
val pageUrl: String = "",
@SerialName("release_notes")
val releaseNotes: String = "",
@SerialName("title")
val title: String = "",
@SerialName("zip_url")
val zipUrl: String = ""
)
@Serializable
data class Releases(
@SerialName("alpha")
val alpha: List<NetworkFirmwareRelease> = listOf(),
@SerialName("stable")
val stable: List<NetworkFirmwareRelease> = listOf()
)
@Serializable
data class NetworkFirmwareReleases(
@SerialName("pullRequests")
val pullRequests: List<NetworkFirmwareRelease> = listOf(),
@SerialName("releases")
val releases: Releases = Releases()
)

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.network.retrofit
import com.geeksville.mesh.network.model.NetworkDeviceHardware
import com.geeksville.mesh.network.model.NetworkFirmwareReleases
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query
interface ApiService {
@GET(".")
suspend fun checkDeviceRegistration(@Query("deviceId") deviceId: String): Response<Unit>
@GET("resource/deviceHardware")
suspend fun getDeviceHardware(): Response<List<NetworkDeviceHardware>>
@GET("/github/firmware/list")
suspend fun getFirmwareReleases(): Response<NetworkFirmwareReleases>
}

View File

@@ -1,2 +1,3 @@
include ':app'
rootProject.name='Meshtastic Android'
include ':network'