Merge branch 'liamcottle-master'

This commit is contained in:
Anton Roslund 2025-01-27 19:02:16 +01:00
commit efdf5f850d
19 changed files with 800 additions and 48 deletions

1
.env.example Normal file
View file

@ -0,0 +1 @@
DATABASE_URL="mysql://root@localhost:3306/meshtastic-map?connection_limit=100"

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.idea/
node_modules node_modules
# Keep environment variables out of version control # Keep environment variables out of version control
.env .env

View file

@ -5,13 +5,15 @@
<a href="https://twitter.com/liamcottle"><img src="https://img.shields.io/badge/Twitter-@liamcottle-%231DA1F2?style=flat&logo=twitter" alt="twitter"/></a> <a href="https://twitter.com/liamcottle"><img src="https://img.shields.io/badge/Twitter-@liamcottle-%231DA1F2?style=flat&logo=twitter" alt="twitter"/></a>
<br/> <br/>
<a href="https://ko-fi.com/liamcottle"><img src="https://img.shields.io/badge/Donate%20a%20Coffee-liamcottle-yellow?style=flat&logo=buy-me-a-coffee" alt="donate on ko-fi"/></a> <a href="https://ko-fi.com/liamcottle"><img src="https://img.shields.io/badge/Donate%20a%20Coffee-liamcottle-yellow?style=flat&logo=buy-me-a-coffee" alt="donate on ko-fi"/></a>
<a href="./donate.md"><img src="https://img.shields.io/badge/Donate%20Bitcoin-3FPBfiEwioWHFix3kZqe5bdU9F5o8mG8dh-%23FF9900?style=flat&logo=bitcoin" alt="donate bitcoin"/></a> <a href="./donate.md"><img src="https://img.shields.io/badge/Donate%20Bitcoin-bc1qy22smke8n4c54evdxmp7lpy9p0e6m9tavtlg2q-%23FF9900?style=flat&logo=bitcoin" alt="donate bitcoin"/></a>
</p> </p>
A map of all Meshtastic nodes heard via MQTT. A map of all Meshtastic nodes heard via MQTT.
My version of the map is available at https://meshtastic.liamcottle.net 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)
<img src="./screenshot.png"> <img src="./screenshot.png">
## How does it work? ## How does it work?

View file

@ -4,6 +4,7 @@ Thank you for considering donating, this helps support my work on this project
## How can I donate? ## How can I donate?
- Bitcoin: 3FPBfiEwioWHFix3kZqe5bdU9F5o8mG8dh - Bitcoin: bc1qy22smke8n4c54evdxmp7lpy9p0e6m9tavtlg2q
- Ethereum: 0xc64CFbA5D0BF7664158c5671F64d446395b3bF3D
- Buy me a Coffee: [https://ko-fi.com/liamcottle](https://ko-fi.com/liamcottle) - 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) - Sponsor on GitHub: [https://github.com/sponsors/liamcottle](https://github.com/sponsors/liamcottle)

View file

@ -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

15
meshtastic-map.service Normal file
View file

@ -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

View file

@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX `nodes_position_updated_at_idx` ON `nodes`(`position_updated_at`);

View file

@ -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;

View file

@ -56,6 +56,7 @@ model Node {
@@index(created_at) @@index(created_at)
@@index(updated_at) @@index(updated_at)
@@index(position_updated_at)
@@index(node_id) @@index(node_id)
@@map("nodes") @@map("nodes")
} }
@ -132,6 +133,10 @@ model EnvironmentMetric {
voltage Decimal? voltage Decimal?
current Decimal? current Decimal?
iaq Int? iaq Int?
wind_direction Int?
wind_speed Decimal?
wind_gust Decimal?
wind_lull Decimal?
created_at DateTime @default(now()) created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt updated_at DateTime @default(now()) @updatedAt

127
src/admin.js Normal file
View file

@ -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);
}
})();

View file

@ -92,23 +92,101 @@ app.get('/api', async (req, res) => {
}, },
{ {
"path": "/api/v1/nodes", "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", "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", "path": "/api/v1/waypoints",
"description": "Meshtastic waypoints in JSON format.", "description": "Waypoints",
}, },
]; ];
const html = links.map((link) => { const linksHtml = links.map((link) => {
return `<li><a href="${link.path}">${link.path}</a> - ${link.description}</li>`; var line = `<li>`;
line += `<a href="${link.path}">${link.path}</a> - ${link.description}`;
line += `<ul>`;
for(const paramKey in (link.params ?? [])){
const paramDescription = link.params[paramKey];
line += "<li>";
line += `?${paramKey}: ${paramDescription}`;
line += `</li>`;
}
line += `</ul>`;
return line;
}).join(""); }).join("");
res.send(html); res.send(`<b>API Docs</b><br/><ul>${linksHtml}</ul>`);
}); });

View file

@ -119,6 +119,11 @@ const optionsList = [
type: Number, type: Number,
description: "If provided, position packets from firmware v2.4 and older will be truncated to this many decimal places.", 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", name: "purge-interval-seconds",
type: Number, type: Number,
@ -221,6 +226,7 @@ const decryptionKeys = options["decryption-keys"] ?? [
const dropPacketsNotOkToMqtt = options["drop-packets-not-ok-to-mqtt"] ?? false; const dropPacketsNotOkToMqtt = options["drop-packets-not-ok-to-mqtt"] ?? false;
const dropPortnumsWithoutBitfield = options["drop-portnums-without-bitfield"] ?? null; const dropPortnumsWithoutBitfield = options["drop-portnums-without-bitfield"] ?? null;
const oldFirmwarePositionPrecision = options["old-firmware-position-precision"] ?? 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 purgeIntervalSeconds = options["purge-interval-seconds"] ?? 10;
const purgeNodesUnheardForSeconds = options["purge-nodes-unheard-for-seconds"] ?? null; const purgeNodesUnheardForSeconds = options["purge-nodes-unheard-for-seconds"] ?? null;
const purgeDeviceMetricsAfterSeconds = options["purge-device-metrics-after-seconds"] ?? null; const purgeDeviceMetricsAfterSeconds = options["purge-device-metrics-after-seconds"] ?? null;
@ -269,6 +275,7 @@ if(purgeIntervalSeconds){
await purgeOldTextMessages(); await purgeOldTextMessages();
await purgeOldTraceroutes(); await purgeOldTraceroutes();
await purgeOldWaypoints(); await purgeOldWaypoints();
await forgetOutdatedNodePositions();
}, purgeIntervalSeconds * 1000); }, 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) { function createNonce(packetId, fromNode) {
// Expand packetId to 64 bits // 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 voltage = telemetry.environmentMetrics.voltage !== 0 ? telemetry.environmentMetrics.voltage : null;
const current = telemetry.environmentMetrics.current !== 0 ? telemetry.environmentMetrics.current : null; const current = telemetry.environmentMetrics.current !== 0 ? telemetry.environmentMetrics.current : null;
const iaq = telemetry.environmentMetrics.iaq !== 0 ? telemetry.environmentMetrics.iaq : 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 // set metrics to update on node table
data.temperature = temperature; data.temperature = temperature;
@ -1105,6 +1155,10 @@ client.on("message", async (topic, message) => {
voltage: voltage, voltage: voltage,
current: current, current: current,
iaq: iaq, iaq: iaq,
wind_direction: windDirection,
wind_speed: windSpeed,
wind_gust: windGust,
wind_lull: windLull,
}, },
}); });
} }

View file

@ -95,6 +95,15 @@ message Config {
* Uses position module configuration to determine TAK PLI broadcast interval. * Uses position module configuration to determine TAK PLI broadcast interval.
*/ */
TAK_TRACKER = 10; 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 * Singapore 923mhz
*/ */
SG_923 = 18; 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 Range - Moderately Fast
*/ */
LONG_MODERATE = 7; 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;
} }
/* /*

View file

@ -339,6 +339,11 @@ enum HardwareModel {
*/ */
HELTEC_HRU_3601 = 23; 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 * B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
*/ */
@ -427,7 +432,7 @@ enum HardwareModel {
DR_DEV = 41; 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; M5STACK = 42;
@ -620,9 +625,50 @@ enum HardwareModel {
* *
*/ */
RP2040_FEATHER_RFM95 = 76; 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/ */
/* 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_COREBASIC = 77;
M5STACK_CORE2 = 78; 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. * 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.

View file

@ -15,27 +15,27 @@ message DeviceMetrics {
/* /*
* 0-100 (>100 means powered) * 0-100 (>100 means powered)
*/ */
uint32 battery_level = 1; optional uint32 battery_level = 1;
/* /*
* Voltage measured * Voltage measured
*/ */
float voltage = 2; optional float voltage = 2;
/* /*
* Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). * 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. * 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) * 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 * Temperature measured
*/ */
float temperature = 1; optional float temperature = 1;
/* /*
* Relative humidity percent measured * Relative humidity percent measured
*/ */
float relative_humidity = 2; optional float relative_humidity = 2;
/* /*
* Barometric pressure in hPA measured * Barometric pressure in hPA measured
*/ */
float barometric_pressure = 3; optional float barometric_pressure = 3;
/* /*
* Gas resistance in MOhm measured * 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) * 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) * 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. * 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. * 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) * Voltage (Ch1)
*/ */
float ch1_voltage = 1; optional float ch1_voltage = 1;
/* /*
* Current (Ch1) * Current (Ch1)
*/ */
float ch1_current = 2; optional float ch1_current = 2;
/* /*
* Voltage (Ch2) * Voltage (Ch2)
*/ */
float ch2_voltage = 3; optional float ch2_voltage = 3;
/* /*
* Current (Ch2) * Current (Ch2)
*/ */
float ch2_current = 4; optional float ch2_current = 4;
/* /*
* Voltage (Ch3) * Voltage (Ch3)
*/ */
float ch3_voltage = 5; optional float ch3_voltage = 5;
/* /*
* Current (Ch3) * Current (Ch3)
*/ */
float ch3_current = 6; optional float ch3_current = 6;
} }
/* /*
@ -121,62 +178,147 @@ message AirQualityMetrics {
/* /*
* Concentration Units Standard PM1.0 * Concentration Units Standard PM1.0
*/ */
uint32 pm10_standard = 1; optional uint32 pm10_standard = 1;
/* /*
* Concentration Units Standard PM2.5 * Concentration Units Standard PM2.5
*/ */
uint32 pm25_standard = 2; optional uint32 pm25_standard = 2;
/* /*
* Concentration Units Standard PM10.0 * Concentration Units Standard PM10.0
*/ */
uint32 pm100_standard = 3; optional uint32 pm100_standard = 3;
/* /*
* Concentration Units Environmental PM1.0 * Concentration Units Environmental PM1.0
*/ */
uint32 pm10_environmental = 4; optional uint32 pm10_environmental = 4;
/* /*
* Concentration Units Environmental PM2.5 * Concentration Units Environmental PM2.5
*/ */
uint32 pm25_environmental = 5; optional uint32 pm25_environmental = 5;
/* /*
* Concentration Units Environmental PM10.0 * Concentration Units Environmental PM10.0
*/ */
uint32 pm100_environmental = 6; optional uint32 pm100_environmental = 6;
/* /*
* 0.3um Particle Count * 0.3um Particle Count
*/ */
uint32 particles_03um = 7; optional uint32 particles_03um = 7;
/* /*
* 0.5um Particle Count * 0.5um Particle Count
*/ */
uint32 particles_05um = 8; optional uint32 particles_05um = 8;
/* /*
* 1.0um Particle Count * 1.0um Particle Count
*/ */
uint32 particles_10um = 9; optional uint32 particles_10um = 9;
/* /*
* 2.5um Particle Count * 2.5um Particle Count
*/ */
uint32 particles_25um = 10; optional uint32 particles_25um = 10;
/* /*
* 5.0um Particle Count * 5.0um Particle Count
*/ */
uint32 particles_50um = 11; optional uint32 particles_50um = 11;
/* /*
* 10.0um Particle Count * 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 * Power Metrics
*/ */
PowerMetrics power_metrics = 5; 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/BMP180 High accuracy temperature and pressure (older Version of BMP280)
*/ */
BMP085 = 15; 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;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -2245,7 +2245,7 @@
}, },
}, },
y: { y: {
min: 0, min: -20,
max: 100, max: 100,
}, },
y1: { y1: {
@ -2718,6 +2718,11 @@
attribution: 'Tiles &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>', attribution: 'Tiles &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
}); });
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 &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
});
var esriWorldImageryTileLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { 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 maxZoom: 21, // esri doesn't have tiles closer than this
attribution: 'Tiles &copy; <a href="https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/">Esri</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>' attribution: 'Tiles &copy; <a href="https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/">Esri</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>'
@ -2737,6 +2742,7 @@
var tileLayers = { var tileLayers = {
"OpenStreetMap": openStreetMapTileLayer, "OpenStreetMap": openStreetMapTileLayer,
"OpenTopoMap": openTopoMapTileLayer,
"Esri Satellite": esriWorldImageryTileLayer, "Esri Satellite": esriWorldImageryTileLayer,
"Google Satellite": googleSatelliteTileLayer, "Google Satellite": googleSatelliteTileLayer,
"Google Hybrid": googleHybridTileLayer, "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) { function showNodeNeighboursThatWeHeard(id) {
cleanUpNodeNeighbours(); cleanUpNodeNeighbours();
@ -3161,12 +3198,16 @@
distance = `${distanceInKilometers} kilometers`; distance = `${distanceInKilometers} kilometers`;
} }
const terrainImageUrl = getTerrainProfileImage(node, neighbourNode);
const tooltip = `<b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b> heard <b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b>` const tooltip = `<b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b> heard <b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b>`
+ `<br/>SNR: ${neighbour.snr}dB` + `<br/>SNR: ${neighbour.snr}dB`
+ `<br/>Distance: ${distance}` + `<br/>Distance: ${distance}`
+ `<br/><br/>ID: ${node.node_id} heard ${neighbourNode.node_id}` + `<br/><br/>ID: ${node.node_id} heard ${neighbourNode.node_id}`
+ `<br/>Hex ID: ${node.node_id_hex} heard ${neighbourNode.node_id_hex}` + `<br/>Hex ID: ${node.node_id_hex} heard ${neighbourNode.node_id_hex}`
+ (node.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : ''); + (node.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '')
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`;
line.bindTooltip(tooltip, { line.bindTooltip(tooltip, {
sticky: true, sticky: true,
@ -3278,12 +3319,16 @@
distance = `${distanceInKilometers} kilometers`; distance = `${distanceInKilometers} kilometers`;
} }
const terrainImageUrl = getTerrainProfileImage(neighbourNode, node);
const tooltip = `<b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b> heard <b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b>` const tooltip = `<b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b> heard <b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b>`
+ `<br/>SNR: ${neighbour.snr}dB` + `<br/>SNR: ${neighbour.snr}dB`
+ `<br/>Distance: ${distance}` + `<br/>Distance: ${distance}`
+ `<br/><br/>ID: ${neighbourNode.node_id} heard ${node.node_id}` + `<br/><br/>ID: ${neighbourNode.node_id} heard ${node.node_id}`
+ `<br/>Hex ID: ${neighbourNode.node_id_hex} heard ${node.node_id_hex}` + `<br/>Hex ID: ${neighbourNode.node_id_hex} heard ${node.node_id_hex}`
+ (neighbourNode.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(neighbourNode.neighbours_updated_at)).fromNow()}` : ''); + (neighbourNode.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(neighbourNode.neighbours_updated_at)).fromNow()}` : '')
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`;
line.bindTooltip(tooltip, { line.bindTooltip(tooltip, {
sticky: true, sticky: true,
@ -3422,6 +3467,7 @@
// add markers for routers and repeaters to routers layer group // add markers for routers and repeaters to routers layer group
if(node.role_name === "ROUTER" if(node.role_name === "ROUTER"
|| node.role_name === "ROUTER_CLIENT" || node.role_name === "ROUTER_CLIENT"
|| node.role_name === "ROUTER_LATE"
|| node.role_name === "REPEATER"){ || node.role_name === "REPEATER"){
nodesRouterLayerGroup.addLayer(marker); nodesRouterLayerGroup.addLayer(marker);
} }
@ -3518,12 +3564,16 @@
distance = `${distanceInKilometers} kilometers`; distance = `${distanceInKilometers} kilometers`;
} }
const terrainImageUrl = getTerrainProfileImage(node, neighbourNode);
const tooltip = `<b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b> heard <b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b>` const tooltip = `<b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b> heard <b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b>`
+ `<br/>SNR: ${neighbour.snr}dB` + `<br/>SNR: ${neighbour.snr}dB`
+ `<br/>Distance: ${distance}` + `<br/>Distance: ${distance}`
+ `<br/><br/>ID: ${node.node_id} heard ${neighbourNode.node_id}` + `<br/><br/>ID: ${node.node_id} heard ${neighbourNode.node_id}`
+ `<br/>Hex ID: ${node.node_id_hex} heard ${neighbourNode.node_id_hex}` + `<br/>Hex ID: ${node.node_id_hex} heard ${neighbourNode.node_id_hex}`
+ (node.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '') + (node.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '')
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`;
line.bindTooltip(tooltip, { line.bindTooltip(tooltip, {
sticky: true, sticky: true,

23
src/utils/node_id_util.js Normal file
View file

@ -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;

View file

@ -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));
});