Merge branch 'liamcottle-master'
This commit is contained in:
commit
efdf5f850d
19 changed files with 800 additions and 48 deletions
1
.env.example
Normal file
1
.env.example
Normal file
|
|
@ -0,0 +1 @@
|
|||
DATABASE_URL="mysql://root@localhost:3306/meshtastic-map?connection_limit=100"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
.idea/
|
||||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<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="./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>
|
||||
|
||||
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)
|
||||
|
||||
<img src="./screenshot.png">
|
||||
|
||||
## How does it work?
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
- Sponsor on GitHub: [https://github.com/sponsors/liamcottle](https://github.com/sponsors/liamcottle)
|
||||
|
|
|
|||
39
meshtastic-map-mqtt.service
Normal file
39
meshtastic-map-mqtt.service
Normal 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
15
meshtastic-map.service
Normal 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
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- CreateIndex
|
||||
CREATE INDEX `nodes_position_updated_at_idx` ON `nodes`(`position_updated_at`);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
127
src/admin.js
Normal file
127
src/admin.js
Normal 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);
|
||||
}
|
||||
|
||||
})();
|
||||
90
src/index.js
90
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 `<li><a href="${link.path}">${link.path}</a> - ${link.description}</li>`;
|
||||
const linksHtml = links.map((link) => {
|
||||
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("");
|
||||
|
||||
res.send(html);
|
||||
res.send(`<b>API Docs</b><br/><ul>${linksHtml}</ul>`);
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
54
src/mqtt.js
54
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
BIN
src/public/images/devices/HELTEC_MESH_NODE_T114.png
Normal file
BIN
src/public/images/devices/HELTEC_MESH_NODE_T114.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
|
|
@ -2245,7 +2245,7 @@
|
|||
},
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
min: -20,
|
||||
max: 100,
|
||||
},
|
||||
y1: {
|
||||
|
|
@ -2718,6 +2718,11 @@
|
|||
attribution: 'Tiles © <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 © <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}', {
|
||||
maxZoom: 21, // esri doesn't have tiles closer than this
|
||||
attribution: 'Tiles © <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 = {
|
||||
"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 = `<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/>Distance: ${distance}`
|
||||
+ `<br/><br/>ID: ${node.node_id} heard ${neighbourNode.node_id}`
|
||||
+ `<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, {
|
||||
sticky: true,
|
||||
|
|
@ -3278,12 +3319,16 @@
|
|||
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>`
|
||||
+ `<br/>SNR: ${neighbour.snr}dB`
|
||||
+ `<br/>Distance: ${distance}`
|
||||
+ `<br/><br/>ID: ${neighbourNode.node_id} heard ${node.node_id}`
|
||||
+ `<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, {
|
||||
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 = `<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/>Distance: ${distance}`
|
||||
+ `<br/><br/>ID: ${node.node_id} heard ${neighbourNode.node_id}`
|
||||
+ `<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()}` : '')
|
||||
|
||||
+ `<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, {
|
||||
sticky: true,
|
||||
opacity: 1,
|
||||
|
|
|
|||
23
src/utils/node_id_util.js
Normal file
23
src/utils/node_id_util.js
Normal 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;
|
||||
9
src/utils/node_id_util.test.js
Normal file
9
src/utils/node_id_util.test.js
Normal 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));
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue