diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..cb7eebb
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+DATABASE_URL="mysql://root@localhost:3306/meshtastic-map?connection_limit=100"
diff --git a/.gitignore b/.gitignore
index 11ddd8d..559c261 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.idea/
node_modules
# Keep environment variables out of version control
.env
diff --git a/README.md b/README.md
index 920df08..854146a 100644
--- a/README.md
+++ b/README.md
@@ -5,13 +5,15 @@
-
+
A map of all Meshtastic nodes heard via MQTT.
My version of the map is available at https://meshtastic.liamcottle.net
+> Check out my new Meshtastic Web Client: [MeshTXT](https://github.com/liamcottle/meshtxt)
+
## How does it work?
diff --git a/donate.md b/donate.md
index 00c96df..f64f99b 100644
--- a/donate.md
+++ b/donate.md
@@ -4,6 +4,7 @@ Thank you for considering donating, this helps support my work on this project
## How can I donate?
-- Bitcoin: 3FPBfiEwioWHFix3kZqe5bdU9F5o8mG8dh
+- Bitcoin: bc1qy22smke8n4c54evdxmp7lpy9p0e6m9tavtlg2q
+- Ethereum: 0xc64CFbA5D0BF7664158c5671F64d446395b3bF3D
- Buy me a Coffee: [https://ko-fi.com/liamcottle](https://ko-fi.com/liamcottle)
-- Sponsor on GitHub: [https://github.com/sponsors/liamcottle](https://github.com/sponsors/liamcottle)
\ No newline at end of file
+- Sponsor on GitHub: [https://github.com/sponsors/liamcottle](https://github.com/sponsors/liamcottle)
diff --git a/meshtastic-map-mqtt.service b/meshtastic-map-mqtt.service
new file mode 100644
index 0000000..8a30052
--- /dev/null
+++ b/meshtastic-map-mqtt.service
@@ -0,0 +1,39 @@
+[Unit]
+Description=meshtastic-map-mqtt
+After=network.target
+StartLimitIntervalSec=0
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=1
+User=liamcottle
+WorkingDirectory=/home/liamcottle/meshtastic-map
+ExecStart=/usr/bin/env node /home/liamcottle/meshtastic-map/src/mqtt.js \
+ --mqtt-broker-url mqtt://127.0.0.1 \
+ --mqtt-username username \
+ --mqtt-password password \
+ --mqtt-client-id meshtastic.example.com \
+ --mqtt-topic 'msh/#' \
+ --collect-positions \
+ --collect-text-messages \
+ --collect-waypoints \
+ --ignore-direct-messages \
+ --purge-interval-seconds 60 \
+ --purge-nodes-unheard-for-seconds 604800 \
+ --purge-device-metrics-after-seconds 604800 \
+ --purge-environment-metrics-after-seconds 604800 \
+ --purge-map-reports-after-seconds 604800 \
+ --purge-neighbour-infos-after-seconds 604800 \
+ --purge-power-metrics-after-seconds 604800 \
+ --purge-positions-after-seconds 604800 \
+ --purge-service-envelopes-after-seconds 604800 \
+ --purge-text-messages-after-seconds 604800 \
+ --purge-traceroutes-after-seconds 604800 \
+ --purge-waypoints-after-seconds 604800 \
+ --forget-outdated-node-positions-after-seconds 604800 \
+ --drop-packets-not-ok-to-mqtt \
+ --old-firmware-position-precision 16
+
+[Install]
+WantedBy=multi-user.target
diff --git a/meshtastic-map.service b/meshtastic-map.service
new file mode 100644
index 0000000..8b83b8d
--- /dev/null
+++ b/meshtastic-map.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=meshtastic-map
+After=network.target
+StartLimitIntervalSec=0
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=1
+User=liamcottle
+WorkingDirectory=/home/liamcottle/meshtastic-map
+ExecStart=/usr/bin/env node /home/liamcottle/meshtastic-map/src/index.js
+
+[Install]
+WantedBy=multi-user.target
diff --git a/prisma/migrations/20241120235736_add_index_to_position_updated_at_column_for_nodes_table/migration.sql b/prisma/migrations/20241120235736_add_index_to_position_updated_at_column_for_nodes_table/migration.sql
new file mode 100644
index 0000000..a574a65
--- /dev/null
+++ b/prisma/migrations/20241120235736_add_index_to_position_updated_at_column_for_nodes_table/migration.sql
@@ -0,0 +1,2 @@
+-- CreateIndex
+CREATE INDEX `nodes_position_updated_at_idx` ON `nodes`(`position_updated_at`);
diff --git a/prisma/migrations/20241217025513_add_wind_columns_to_environment_metrics_table/migration.sql b/prisma/migrations/20241217025513_add_wind_columns_to_environment_metrics_table/migration.sql
new file mode 100644
index 0000000..04070de
--- /dev/null
+++ b/prisma/migrations/20241217025513_add_wind_columns_to_environment_metrics_table/migration.sql
@@ -0,0 +1,5 @@
+-- AlterTable
+ALTER TABLE `environment_metrics` ADD COLUMN `wind_direction` INTEGER NULL,
+ ADD COLUMN `wind_gust` DECIMAL(65, 30) NULL,
+ ADD COLUMN `wind_lull` DECIMAL(65, 30) NULL,
+ ADD COLUMN `wind_speed` DECIMAL(65, 30) NULL;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index a152277..6741657 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -56,6 +56,7 @@ model Node {
@@index(created_at)
@@index(updated_at)
+ @@index(position_updated_at)
@@index(node_id)
@@map("nodes")
}
@@ -132,6 +133,10 @@ model EnvironmentMetric {
voltage Decimal?
current Decimal?
iaq Int?
+ wind_direction Int?
+ wind_speed Decimal?
+ wind_gust Decimal?
+ wind_lull Decimal?
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
diff --git a/src/admin.js b/src/admin.js
new file mode 100644
index 0000000..e9fbbef
--- /dev/null
+++ b/src/admin.js
@@ -0,0 +1,127 @@
+// node src/admin.js --purge-node-id 123
+// node src/admin.js --purge-node-id '!AABBCCDD'
+
+const commandLineArgs = require("command-line-args");
+const commandLineUsage = require("command-line-usage");
+
+// create prisma db client
+const { PrismaClient } = require("@prisma/client");
+const NodeIdUtil = require("./utils/node_id_util");
+const prisma = new PrismaClient();
+
+const optionsList = [
+ {
+ name: 'help',
+ alias: 'h',
+ type: Boolean,
+ description: 'Display this usage guide.'
+ },
+ {
+ name: "purge-node-id",
+ type: String,
+ description: "Purges all records for the provided node id.",
+ },
+];
+
+// parse command line args
+const options = commandLineArgs(optionsList);
+
+// show help
+if(options.help){
+ const usage = commandLineUsage([
+ {
+ header: 'Meshtastic Map Admin',
+ content: 'Command line admin tool for the Meshtastic Map',
+ },
+ {
+ header: 'Options',
+ optionList: optionsList,
+ },
+ ]);
+ console.log(usage);
+ return;
+}
+
+// get options and fallback to default values
+const purgeNodeId = options["purge-node-id"] ?? null;
+
+async function purgeNodeById(nodeId) {
+
+ // convert to numeric id
+ nodeId = NodeIdUtil.convertToNumeric(nodeId);
+
+ // purge environment metrics
+ await prisma.environmentMetric.deleteMany({
+ where: {
+ node_id: nodeId,
+ },
+ });
+
+ // purge map reports
+ await prisma.mapReport.deleteMany({
+ where: {
+ node_id: nodeId,
+ },
+ });
+
+ // purge neighbour infos
+ await prisma.neighbourInfo.deleteMany({
+ where: {
+ node_id: nodeId,
+ },
+ });
+
+ // purge this node
+ await prisma.node.deleteMany({
+ where: {
+ node_id: nodeId,
+ },
+ });
+
+ // purge positions
+ await prisma.position.deleteMany({
+ where: {
+ node_id: nodeId,
+ },
+ });
+
+ // purge power metrics
+ await prisma.powerMetric.deleteMany({
+ where: {
+ node_id: nodeId,
+ },
+ });
+
+ // purge text messages
+ await prisma.textMessage.deleteMany({
+ where: {
+ from: nodeId,
+ },
+ });
+
+ // purge traceroutes
+ await prisma.traceRoute.deleteMany({
+ where: {
+ from: nodeId,
+ },
+ });
+
+ // purge waypoints
+ await prisma.waypoint.deleteMany({
+ where: {
+ from: nodeId,
+ },
+ });
+
+ console.log(`✅ Node '${nodeId}' has been purged from the database.`);
+
+}
+
+(async () => {
+
+ // purge node by id
+ if(purgeNodeId){
+ await purgeNodeById(purgeNodeId);
+ }
+
+})();
diff --git a/src/index.js b/src/index.js
index a63a626..61bd055 100644
--- a/src/index.js
+++ b/src/index.js
@@ -92,23 +92,101 @@ app.get('/api', async (req, res) => {
},
{
"path": "/api/v1/nodes",
- "description": "Meshtastic nodes in JSON format.",
+ "description": "All meshtastic nodes",
+ "params": {
+ "role": "Filter by role",
+ "hardware_model": "Filter by hardware model",
+ },
+ },
+ {
+ "path": "/api/v1/nodes/:nodeId",
+ "description": "A specific meshtastic node",
+ },
+ {
+ "path": "/api/v1/nodes/:nodeId/device-metrics",
+ "description": "Device metrics for a meshtastic node",
+ "params": {
+ "count": "How many results to return",
+ "time_from": "Only include metrics created after this unix timestamp (milliseconds)",
+ "time_to": "Only include metrics created before this unix timestamp (milliseconds)",
+ },
+ },
+ {
+ "path": "/api/v1/nodes/:nodeId/environment-metrics",
+ "description": "Environment metrics for a meshtastic node",
+ "params": {
+ "count": "How many results to return",
+ "time_from": "Only include metrics created after this unix timestamp (milliseconds)",
+ "time_to": "Only include metrics created before this unix timestamp (milliseconds)",
+ },
+ },
+ {
+ "path": "/api/v1/nodes/:nodeId/power-metrics",
+ "description": "Power metrics for a meshtastic node",
+ "params": {
+ "count": "How many results to return",
+ "time_from": "Only include metrics created after this unix timestamp (milliseconds)",
+ "time_to": "Only include metrics created before this unix timestamp (milliseconds)",
+ },
+ },
+ {
+ "path": "/api/v1/nodes/:nodeId/neighbours",
+ "description": "Neighbours for a meshtastic node",
+ },
+ {
+ "path": "/api/v1/nodes/:nodeId/traceroutes",
+ "description": "Trace routes for a meshtastic node",
+ },
+ {
+ "path": "/api/v1/nodes/:nodeId/position-history",
+ "description": "Position history for a meshtastic node",
+ "params": {
+ "time_from": "Only include positions created after this unix timestamp (milliseconds)",
+ "time_to": "Only include positions created before this unix timestamp (milliseconds)",
+ },
},
{
"path": "/api/v1/stats/hardware-models",
- "description": "Database statistics about hardware models in JSON format.",
+ "description": "Database statistics about known hardware models",
+ },
+ {
+ "path": "/api/v1/text-messages",
+ "description": "Text messages",
+ "params": {
+ "to": "Only include messages to this node id",
+ "from": "Only include messages from this node id",
+ "channel_id": "Only include messages for this channel id",
+ "gateway_id": "Only include messages gated to mqtt by this node id",
+ "last_id": "Only include messages before or after this id, based on results order",
+ "count": "How many results to return",
+ "order": "Order to return results in: asc, desc",
+ },
+ },
+ {
+ "path": "/api/v1/text-messages/embed",
+ "description": "Text messages rendered as an embeddable HTML page.",
},
{
"path": "/api/v1/waypoints",
- "description": "Meshtastic waypoints in JSON format.",
+ "description": "Waypoints",
},
];
- const html = links.map((link) => {
- return `${link.path} - ${link.description}`;
+ const linksHtml = links.map((link) => {
+ var line = ``;
+ line += `${link.path} - ${link.description}`;
+ line += ``;
+ for(const paramKey in (link.params ?? [])){
+ const paramDescription = link.params[paramKey];
+ line += "- ";
+ line += `?${paramKey}: ${paramDescription}`;
+ line += `
`;
+ }
+ line += `
`;
+ return line;
}).join("");
- res.send(html);
+ res.send(`API Docs
`);
});
diff --git a/src/mqtt.js b/src/mqtt.js
index 379ac77..f960619 100644
--- a/src/mqtt.js
+++ b/src/mqtt.js
@@ -119,6 +119,11 @@ const optionsList = [
type: Number,
description: "If provided, position packets from firmware v2.4 and older will be truncated to this many decimal places.",
},
+ {
+ name: "forget-outdated-node-positions-after-seconds",
+ type: Number,
+ description: "If provided, nodes that haven't sent a position report in this time will have their current position cleared.",
+ },
{
name: "purge-interval-seconds",
type: Number,
@@ -221,6 +226,7 @@ const decryptionKeys = options["decryption-keys"] ?? [
const dropPacketsNotOkToMqtt = options["drop-packets-not-ok-to-mqtt"] ?? false;
const dropPortnumsWithoutBitfield = options["drop-portnums-without-bitfield"] ?? null;
const oldFirmwarePositionPrecision = options["old-firmware-position-precision"] ?? null;
+const forgetOutdatedNodePositionsAfterSeconds = options["forget-outdated-node-positions-after-seconds"] ?? null;
const purgeIntervalSeconds = options["purge-interval-seconds"] ?? 10;
const purgeNodesUnheardForSeconds = options["purge-nodes-unheard-for-seconds"] ?? null;
const purgeDeviceMetricsAfterSeconds = options["purge-device-metrics-after-seconds"] ?? null;
@@ -269,6 +275,7 @@ if(purgeIntervalSeconds){
await purgeOldTextMessages();
await purgeOldTraceroutes();
await purgeOldWaypoints();
+ await forgetOutdatedNodePositions();
}, purgeIntervalSeconds * 1000);
}
@@ -558,6 +565,45 @@ async function purgeOldWaypoints() {
}
+/**
+ * Clears the current position stored for nodes if the position hasn't been updated within the configured timeframe.
+ * This allows the node position to drop off the map if the user disabled position reporting, but still wants telemetry lookup etc
+ */
+async function forgetOutdatedNodePositions() {
+
+ // make sure seconds provided
+ if(!forgetOutdatedNodePositionsAfterSeconds){
+ return;
+ }
+
+ // clear latitude/longitude/altitude for nodes that haven't updated their position in the configured timeframe
+ try {
+ await prisma.node.updateMany({
+ where: {
+ position_updated_at: {
+ // position_updated_at before x seconds ago
+ lt: new Date(Date.now() - forgetOutdatedNodePositionsAfterSeconds * 1000),
+ },
+ // don't forget outdated node positions for nodes that don't actually have a position set
+ // otherwise the updated_at is updated, when nothing changed
+ NOT: {
+ latitude: null,
+ longitude: null,
+ altitude: null,
+ },
+ },
+ data: {
+ latitude: null,
+ longitude: null,
+ altitude: null,
+ },
+ });
+ } catch(e) {
+ // do nothing
+ }
+
+}
+
function createNonce(packetId, fromNode) {
// Expand packetId to 64 bits
@@ -1072,6 +1118,10 @@ client.on("message", async (topic, message) => {
const voltage = telemetry.environmentMetrics.voltage !== 0 ? telemetry.environmentMetrics.voltage : null;
const current = telemetry.environmentMetrics.current !== 0 ? telemetry.environmentMetrics.current : null;
const iaq = telemetry.environmentMetrics.iaq !== 0 ? telemetry.environmentMetrics.iaq : null;
+ const windDirection = telemetry.environmentMetrics.windDirection;
+ const windSpeed = telemetry.environmentMetrics.windSpeed;
+ const windGust = telemetry.environmentMetrics.windGust;
+ const windLull = telemetry.environmentMetrics.windLull;
// set metrics to update on node table
data.temperature = temperature;
@@ -1105,6 +1155,10 @@ client.on("message", async (topic, message) => {
voltage: voltage,
current: current,
iaq: iaq,
+ wind_direction: windDirection,
+ wind_speed: windSpeed,
+ wind_gust: windGust,
+ wind_lull: windLull,
},
});
}
diff --git a/src/protos/meshtastic/config.proto b/src/protos/meshtastic/config.proto
index 7ebbe45..9bca4d2 100644
--- a/src/protos/meshtastic/config.proto
+++ b/src/protos/meshtastic/config.proto
@@ -95,6 +95,15 @@ message Config {
* Uses position module configuration to determine TAK PLI broadcast interval.
*/
TAK_TRACKER = 10;
+
+ /*
+ * Description: Will always rebroadcast packets, but will do so after all other modes.
+ * Technical Details: Used for router nodes that are intended to provide additional coverage
+ * in areas not already covered by other routers, or to bridge around problematic terrain,
+ * but should not be given priority over other routers in order to avoid unnecessaraily
+ * consuming hops.
+ */
+ ROUTER_LATE = 11;
}
/*
@@ -746,6 +755,21 @@ message Config {
* Singapore 923mhz
*/
SG_923 = 18;
+
+ /*
+ * Philippines 433mhz
+ */
+ PH_433 = 19;
+
+ /*
+ * Philippines 868mhz
+ */
+ PH_868 = 20;
+
+ /*
+ * Philippines 915mhz
+ */
+ PH_915 = 21;
}
/*
@@ -792,6 +816,13 @@ message Config {
* Long Range - Moderately Fast
*/
LONG_MODERATE = 7;
+
+ /*
+ * Short Range - Turbo
+ * This is the fastest preset and the only one with 500kHz bandwidth.
+ * It is not legal to use in all regions due to this wider bandwidth.
+ */
+ SHORT_TURBO = 8;
}
/*
diff --git a/src/protos/meshtastic/mesh.proto b/src/protos/meshtastic/mesh.proto
index ad4155c..7f8b7e5 100644
--- a/src/protos/meshtastic/mesh.proto
+++ b/src/protos/meshtastic/mesh.proto
@@ -339,6 +339,11 @@ enum HardwareModel {
*/
HELTEC_HRU_3601 = 23;
+ /*
+ * Heltec Wireless Bridge
+ */
+ HELTEC_WIRELESS_BRIDGE = 24;
+
/*
* B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
*/
@@ -427,7 +432,7 @@ enum HardwareModel {
DR_DEV = 41;
/*
- * M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, Paper) https://m5stack.com/
+ * M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/
*/
M5STACK = 42;
@@ -620,9 +625,50 @@ enum HardwareModel {
*
*/
RP2040_FEATHER_RFM95 = 76;
- /* M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, Paper) https://m5stack.com/ */
- M5STACK_COREBASIC=77;
- M5STACK_CORE2=78;
+
+ /* M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ */
+ M5STACK_COREBASIC = 77;
+ M5STACK_CORE2 = 78;
+
+ /* Pico2 with Waveshare Hat, same as Pico */
+ RPI_PICO2 = 79;
+
+ /* M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ */
+ M5STACK_CORES3 = 80;
+
+ /* Seeed XIAO S3 DK*/
+ SEEED_XIAO_S3 = 81;
+
+ /*
+ * Nordic nRF52840+Semtech SX1262 LoRa BLE Combo Module. nRF52840+SX1262 MS24SF1
+ */
+ MS24SF1 = 82;
+
+ /*
+ * Lilygo TLora-C6 with the new ESP32-C6 MCU
+ */
+ TLORA_C6 = 83;
+
+ /*
+ * WisMesh Tap
+ * RAK-4631 w/ TFT in injection modled case
+ */
+ WISMESH_TAP = 84;
+
+ /*
+ * Similar to PORTDUINO but used by Routastic devices, this is not any
+ * particular device and does not run Meshtastic's code but supports
+ * the same frame format.
+ * Runs on linux, see https://github.com/Jorropo/routastic
+ */
+ ROUTASTIC = 85;
+
+ /*
+ * Mesh-Tab, esp32 based
+ * https://github.com/valzzu/Mesh-Tab
+ */
+ MESH_TAB = 86;
+
/*
* ------------------------------------------------------------------------------------------------------------------------------------------
* Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
diff --git a/src/protos/meshtastic/telemetry.proto b/src/protos/meshtastic/telemetry.proto
index bd94666..78c0e4f 100644
--- a/src/protos/meshtastic/telemetry.proto
+++ b/src/protos/meshtastic/telemetry.proto
@@ -15,27 +15,27 @@ message DeviceMetrics {
/*
* 0-100 (>100 means powered)
*/
- uint32 battery_level = 1;
+ optional uint32 battery_level = 1;
/*
* Voltage measured
*/
- float voltage = 2;
+ optional float voltage = 2;
/*
* Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise).
*/
- float channel_utilization = 3;
+ optional float channel_utilization = 3;
/*
* Percent of airtime for transmission used within the last hour.
*/
- float air_util_tx = 4;
+ optional float air_util_tx = 4;
/*
* How long the device has been running since the last reboot (in seconds)
*/
- uint32 uptime_seconds = 5;
+ optional uint32 uptime_seconds = 5;
}
/*
@@ -45,38 +45,95 @@ message EnvironmentMetrics {
/*
* Temperature measured
*/
- float temperature = 1;
+ optional float temperature = 1;
/*
* Relative humidity percent measured
*/
- float relative_humidity = 2;
+ optional float relative_humidity = 2;
/*
* Barometric pressure in hPA measured
*/
- float barometric_pressure = 3;
+ optional float barometric_pressure = 3;
/*
* Gas resistance in MOhm measured
*/
- float gas_resistance = 4;
+ optional float gas_resistance = 4;
/*
* Voltage measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x)
*/
- float voltage = 5;
+ optional float voltage = 5;
/*
* Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x)
*/
- float current = 6;
+ optional float current = 6;
- /*
+ /*
* relative scale IAQ value as measured by Bosch BME680 . value 0-500.
* Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here.
*/
- uint32 iaq = 7;
+ optional uint32 iaq = 7;
+
+ /*
+ * RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm.
+ */
+ optional float distance = 8;
+
+ /*
+ * VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
+ */
+ optional float lux = 9;
+
+ /*
+ * VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor.
+ */
+ optional float white_lux = 10;
+
+ /*
+ * Infrared lux
+ */
+ optional float ir_lux = 11;
+
+ /*
+ * Ultraviolet lux
+ */
+ optional float uv_lux = 12;
+
+ /*
+ * Wind direction in degrees
+ * 0 degrees = North, 90 = East, etc...
+ */
+ optional uint32 wind_direction = 13;
+
+ /*
+ * Wind speed in m/s
+ */
+ optional float wind_speed = 14;
+
+ /*
+ * Weight in KG
+ */
+ optional float weight = 15;
+
+ /*
+ * Wind gust in m/s
+ */
+ optional float wind_gust = 16;
+
+ /*
+ * Wind lull in m/s
+ */
+ optional float wind_lull = 17;
+
+ /*
+ * Radiation in µR/h
+ */
+ optional float radiation = 18;
+
}
/*
@@ -86,32 +143,32 @@ message PowerMetrics {
/*
* Voltage (Ch1)
*/
- float ch1_voltage = 1;
+ optional float ch1_voltage = 1;
/*
* Current (Ch1)
*/
- float ch1_current = 2;
+ optional float ch1_current = 2;
/*
* Voltage (Ch2)
*/
- float ch2_voltage = 3;
+ optional float ch2_voltage = 3;
/*
* Current (Ch2)
*/
- float ch2_current = 4;
+ optional float ch2_current = 4;
/*
* Voltage (Ch3)
*/
- float ch3_voltage = 5;
+ optional float ch3_voltage = 5;
/*
* Current (Ch3)
*/
- float ch3_current = 6;
+ optional float ch3_current = 6;
}
/*
@@ -121,62 +178,147 @@ message AirQualityMetrics {
/*
* Concentration Units Standard PM1.0
*/
- uint32 pm10_standard = 1;
+ optional uint32 pm10_standard = 1;
/*
* Concentration Units Standard PM2.5
*/
- uint32 pm25_standard = 2;
+ optional uint32 pm25_standard = 2;
/*
* Concentration Units Standard PM10.0
*/
- uint32 pm100_standard = 3;
+ optional uint32 pm100_standard = 3;
/*
* Concentration Units Environmental PM1.0
*/
- uint32 pm10_environmental = 4;
+ optional uint32 pm10_environmental = 4;
/*
* Concentration Units Environmental PM2.5
*/
- uint32 pm25_environmental = 5;
+ optional uint32 pm25_environmental = 5;
/*
* Concentration Units Environmental PM10.0
*/
- uint32 pm100_environmental = 6;
+ optional uint32 pm100_environmental = 6;
/*
* 0.3um Particle Count
*/
- uint32 particles_03um = 7;
+ optional uint32 particles_03um = 7;
/*
* 0.5um Particle Count
*/
- uint32 particles_05um = 8;
+ optional uint32 particles_05um = 8;
/*
* 1.0um Particle Count
*/
- uint32 particles_10um = 9;
+ optional uint32 particles_10um = 9;
/*
* 2.5um Particle Count
*/
- uint32 particles_25um = 10;
+ optional uint32 particles_25um = 10;
/*
* 5.0um Particle Count
*/
- uint32 particles_50um = 11;
+ optional uint32 particles_50um = 11;
/*
* 10.0um Particle Count
*/
- uint32 particles_100um = 12;
+ optional uint32 particles_100um = 12;
+
+ /*
+ * 10.0um Particle Count
+ */
+ optional uint32 co2 = 13;
+}
+
+/*
+ * Local device mesh statistics
+ */
+message LocalStats {
+ /*
+ * How long the device has been running since the last reboot (in seconds)
+ */
+ uint32 uptime_seconds = 1;
+ /*
+ * Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise).
+ */
+ float channel_utilization = 2;
+ /*
+ * Percent of airtime for transmission used within the last hour.
+ */
+ float air_util_tx = 3;
+
+ /*
+ * Number of packets sent
+ */
+ uint32 num_packets_tx = 4;
+
+ /*
+ * Number of packets received (both good and bad)
+ */
+ uint32 num_packets_rx = 5;
+
+ /*
+ * Number of packets received that are malformed or violate the protocol
+ */
+ uint32 num_packets_rx_bad = 6;
+
+ /*
+ * Number of nodes online (in the past 2 hours)
+ */
+ uint32 num_online_nodes = 7;
+
+ /*
+ * Number of nodes total
+ */
+ uint32 num_total_nodes = 8;
+
+ /*
+ * Number of received packets that were duplicates (due to multiple nodes relaying).
+ * If this number is high, there are nodes in the mesh relaying packets when it's unnecessary, for example due to the ROUTER/REPEATER role.
+ */
+ uint32 num_rx_dupe = 9;
+
+ /*
+ * Number of packets we transmitted that were a relay for others (not originating from ourselves).
+ */
+ uint32 num_tx_relay = 10;
+
+ /*
+ * Number of times we canceled a packet to be relayed, because someone else did it before us.
+ * This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you.
+ */
+ uint32 num_tx_relay_canceled = 11;
+}
+
+/*
+ * Health telemetry metrics
+ */
+message HealthMetrics {
+ /*
+ * Heart rate (beats per minute)
+ */
+ optional uint32 heart_bpm = 1;
+
+ /*
+ * SpO2 (blood oxygen saturation) level
+ */
+ optional uint32 spO2 = 2;
+
+ /*
+ * Body temperature in degrees Celsius
+ */
+ optional float temperature = 3;
}
/*
@@ -208,6 +350,16 @@ message Telemetry {
* Power Metrics
*/
PowerMetrics power_metrics = 5;
+
+ /*
+ * Local device mesh statistics
+ */
+ LocalStats local_stats = 6;
+
+ /*
+ * Health telemetry metrics
+ */
+ HealthMetrics health_metrics = 7;
}
}
@@ -294,4 +446,115 @@ enum TelemetrySensorType {
* BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280)
*/
BMP085 = 15;
+
+ /*
+ * RCWL-9620 Doppler Radar Distance Sensor, used for water level detection
+ */
+ RCWL9620 = 16;
+
+ /*
+ * Sensirion High accuracy temperature and humidity
+ */
+ SHT4X = 17;
+
+ /*
+ * VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
+ */
+ VEML7700 = 18;
+
+ /*
+ * MLX90632 non-contact IR temperature sensor.
+ */
+ MLX90632 = 19;
+
+ /*
+ * TI OPT3001 Ambient Light Sensor
+ */
+ OPT3001 = 20;
+
+ /*
+ * Lite On LTR-390UV-01 UV Light Sensor
+ */
+ LTR390UV = 21;
+
+ /*
+ * AMS TSL25911FN RGB Light Sensor
+ */
+ TSL25911FN = 22;
+
+ /*
+ * AHT10 Integrated temperature and humidity sensor
+ */
+ AHT10 = 23;
+
+ /*
+ * DFRobot Lark Weather station (temperature, humidity, pressure, wind speed and direction)
+ */
+ DFROBOT_LARK = 24;
+
+ /*
+ * NAU7802 Scale Chip or compatible
+ */
+ NAU7802 = 25;
+
+ /*
+ * BMP3XX High accuracy temperature and pressure
+ */
+ BMP3XX = 26;
+
+ /*
+ * ICM-20948 9-Axis digital motion processor
+ */
+ ICM20948 = 27;
+
+ /*
+ * MAX17048 1S lipo battery sensor (voltage, state of charge, time to go)
+ */
+ MAX17048 = 28;
+
+ /*
+ * Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor
+ */
+ CUSTOM_SENSOR = 29;
+
+ /*
+ * MAX30102 Pulse Oximeter and Heart-Rate Sensor
+ */
+ MAX30102 = 30;
+
+ /*
+ * MLX90614 non-contact IR temperature sensor
+ */
+ MLX90614 = 31;
+
+ /*
+ * SCD40/SCD41 CO2, humidity, temperature sensor
+ */
+ SCD4X = 32;
+
+ /*
+ * ClimateGuard RadSens, radiation, Geiger-Muller Tube
+ */
+ RADSENS = 33;
+
+ /*
+ * High accuracy current and voltage
+ */
+ INA226 = 34;
+
}
+
+/*
+ * NAU7802 Telemetry configuration, for saving to flash
+ */
+message Nau7802Config {
+ /*
+ * The offset setting for the NAU7802
+ */
+ int32 zeroOffset = 1;
+
+ /*
+ * The calibration factor for the NAU7802
+ */
+ float calibrationFactor = 2;
+}
\ No newline at end of file
diff --git a/src/public/images/devices/HELTEC_MESH_NODE_T114.png b/src/public/images/devices/HELTEC_MESH_NODE_T114.png
new file mode 100644
index 0000000..aaa09b5
Binary files /dev/null and b/src/public/images/devices/HELTEC_MESH_NODE_T114.png differ
diff --git a/src/public/index.html b/src/public/index.html
index 98e4d3a..7291586 100644
--- a/src/public/index.html
+++ b/src/public/index.html
@@ -2245,7 +2245,7 @@
},
},
y: {
- min: 0,
+ min: -20,
max: 100,
},
y1: {
@@ -2718,6 +2718,11 @@
attribution: 'Tiles © OpenStreetMap | Data from Meshtastic',
});
+ var openTopoMapTileLayer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
+ maxZoom: 17, // open topo map doesn't have tiles closer than this
+ attribution: 'Tiles © OpenStreetMap',
+ });
+
var esriWorldImageryTileLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 21, // esri doesn't have tiles closer than this
attribution: 'Tiles © Esri | Data from Meshtastic'
@@ -2737,6 +2742,7 @@
var tileLayers = {
"OpenStreetMap": openStreetMapTileLayer,
+ "OpenTopoMap": openTopoMapTileLayer,
"Esri Satellite": esriWorldImageryTileLayer,
"Google Satellite": googleSatelliteTileLayer,
"Google Hybrid": googleHybridTileLayer,
@@ -3082,6 +3088,37 @@
}
+ function getTerrainProfileImage(node1, node2) {
+
+ // line colour between nodes
+ const lineColour = "0000FF"; // blue
+
+ // node 1 (left side of image)
+ const node1MarkerColour = "0000FF"; // blue
+ const node1Latitude = node1.latitude;
+ const node1Longitude = node1.longitude;
+ const node1ElevationMSL = ""; // node1.altitude ?? "";
+
+ // node 2 (right side of image)
+ const node2MarkerColour = "0000FF"; // blue
+ const node2Latitude = node2.latitude;
+ const node2Longitude = node2.longitude;
+ const node2ElevationMSL = ""; // node2.altitude ?? "";
+
+ // generate terrain profile image url
+ return "https://heywhatsthat.com/bin/profile-0904.cgi?" + new URLSearchParams({
+ src: "meshtastic.liamcottle.net",
+ axes: 1, // include grid lines and a scale
+ metric: 1, // show metric units
+ curvature: 0, // don't include the curvature of the earth in the graphic
+ width: 500,
+ height: 200,
+ pt0: `${node1Latitude},${node1Longitude},${lineColour},${node1ElevationMSL},${node1MarkerColour}`,
+ pt1: `${node2Latitude},${node2Longitude},${lineColour},${node2ElevationMSL},${node2MarkerColour}`,
+ }).toString();
+
+ }
+
function showNodeNeighboursThatWeHeard(id) {
cleanUpNodeNeighbours();
@@ -3161,12 +3198,16 @@
distance = `${distanceInKilometers} kilometers`;
}
+ const terrainImageUrl = getTerrainProfileImage(node, neighbourNode);
+
const tooltip = `[${escapeString(node.short_name)}] ${escapeString(node.long_name)} heard [${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}`
+ `
SNR: ${neighbour.snr}dB`
+ `
Distance: ${distance}`
+ `
ID: ${node.node_id} heard ${neighbourNode.node_id}`
+ `
Hex ID: ${node.node_id_hex} heard ${neighbourNode.node_id_hex}`
- + (node.neighbours_updated_at ? `
Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '');
+ + (node.neighbours_updated_at ? `
Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '')
+ + `
Terrain images from HeyWhatsThat.com`
+ + `
`;
line.bindTooltip(tooltip, {
sticky: true,
@@ -3278,12 +3319,16 @@
distance = `${distanceInKilometers} kilometers`;
}
+ const terrainImageUrl = getTerrainProfileImage(neighbourNode, node);
+
const tooltip = `[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)} heard [${escapeString(node.short_name)}] ${escapeString(node.long_name)}`
+ `
SNR: ${neighbour.snr}dB`
+ `
Distance: ${distance}`
+ `
ID: ${neighbourNode.node_id} heard ${node.node_id}`
+ `
Hex ID: ${neighbourNode.node_id_hex} heard ${node.node_id_hex}`
- + (neighbourNode.neighbours_updated_at ? `
Updated: ${moment(new Date(neighbourNode.neighbours_updated_at)).fromNow()}` : '');
+ + (neighbourNode.neighbours_updated_at ? `
Updated: ${moment(new Date(neighbourNode.neighbours_updated_at)).fromNow()}` : '')
+ + `
Terrain images from HeyWhatsThat.com`
+ + `
`;
line.bindTooltip(tooltip, {
sticky: true,
@@ -3422,6 +3467,7 @@
// add markers for routers and repeaters to routers layer group
if(node.role_name === "ROUTER"
|| node.role_name === "ROUTER_CLIENT"
+ || node.role_name === "ROUTER_LATE"
|| node.role_name === "REPEATER"){
nodesRouterLayerGroup.addLayer(marker);
}
@@ -3518,13 +3564,17 @@
distance = `${distanceInKilometers} kilometers`;
}
+ const terrainImageUrl = getTerrainProfileImage(node, neighbourNode);
+
const tooltip = `[${escapeString(node.short_name)}] ${escapeString(node.long_name)} heard [${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}`
+ `
SNR: ${neighbour.snr}dB`
+ `
Distance: ${distance}`
+ `
ID: ${node.node_id} heard ${neighbourNode.node_id}`
+ `
Hex ID: ${node.node_id_hex} heard ${neighbourNode.node_id_hex}`
+ (node.neighbours_updated_at ? `
Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '')
-
+ + `
Terrain images from HeyWhatsThat.com`
+ + `
`;
+
line.bindTooltip(tooltip, {
sticky: true,
opacity: 1,
diff --git a/src/utils/node_id_util.js b/src/utils/node_id_util.js
new file mode 100644
index 0000000..5397df1
--- /dev/null
+++ b/src/utils/node_id_util.js
@@ -0,0 +1,23 @@
+class NodeIdUtil {
+
+ /**
+ * Converts the provided hex id to a numeric id, for example: !FFFFFFFF to 4294967295
+ * Anything else will be converted as is to a BigInt, for example "4294967295" to 4294967295
+ * @param hexIdOrNumber a node id in hex format with a prepended "!", or a numeric node id as a string or number
+ * @returns {bigint} the node id in numeric form
+ */
+ static convertToNumeric(hexIdOrNumber) {
+
+ // check if this is a hex id, and convert to numeric
+ if(hexIdOrNumber.toString().startsWith("!")){
+ return BigInt('0x' + hexIdOrNumber.replaceAll("!", ""));
+ }
+
+ // convert string or number to numeric
+ return BigInt(hexIdOrNumber);
+
+ }
+
+}
+
+module.exports = NodeIdUtil;
diff --git a/src/utils/node_id_util.test.js b/src/utils/node_id_util.test.js
new file mode 100644
index 0000000..494c3bb
--- /dev/null
+++ b/src/utils/node_id_util.test.js
@@ -0,0 +1,9 @@
+const NodeIdUtil = require("./node_id_util");
+
+test('can convert hex id to numeric id', () => {
+ expect(NodeIdUtil.convertToNumeric("!FFFFFFFF")).toBe(BigInt(4294967295));
+});
+
+test('can convert numeric id to numeric id', () => {
+ expect(NodeIdUtil.convertToNumeric(4294967295)).toBe(BigInt(4294967295));
+});