Compare commits

..

21 Commits

Author SHA1 Message Date
Anton Roslund
eda9a12443 improve docker build time 2025-08-10 20:10:28 +02:00
liamcottle
09a2bcb3ad update announcement 2025-06-15 19:01:13 +12:00
liamcottle
de1cfd4222 update express to v5.0.0 2025-06-15 16:42:21 +12:00
liamcottle
1c1b77b3ea add note about mqtt collector 2025-06-15 16:40:08 +12:00
liamcottle
2a1ef2131a users must provide their own protobuf schema files 2025-06-15 16:35:16 +12:00
liamcottle
8a43c9d3d1 remove protobuf from server 2025-06-15 16:26:33 +12:00
liamcottle
9a18ca1057 add external to .gitignore 2025-06-15 15:28:16 +12:00
liamcottle
ffe1c6c30a remove protobufs 2025-06-15 15:17:20 +12:00
liamcottle
1aa32cfa35 use purple lines between position history markers 2025-04-25 17:36:52 +12:00
liamcottle
825b62c5bb add openssl to dockerfile 2025-04-25 17:29:45 +12:00
Liam Cottle
b87a7b2f27 Merge pull request #92 from sgtwilko/StopGraphFromCuttingOffValues
Change Voltage chart to use suggested min/max (#1)
2025-04-25 17:26:56 +12:00
liamcottle
2ab169b4ff update device image for RP2040_LORA 2025-04-25 17:20:31 +12:00
liamcottle
821d6177c3 add device image for SEEED_XIAO_S3 2025-04-25 17:12:27 +12:00
liamcottle
8acc4696db add device image for STATION_G2 2025-04-25 17:10:25 +12:00
liamcottle
cd6a99a179 remove white background from device images 2025-04-25 17:07:22 +12:00
liamcottle
90dc3ae449 rotate and optimise image 2025-04-25 16:38:00 +12:00
Liam Cottle
07e362745a Merge pull request #96 from valzzu/master
add image for nrf52 diy nodes
2025-04-25 16:37:11 +12:00
Liam Cottle
f6d14b8f95 Merge pull request #97 from dieseltravis/patch-1
replace \u00BA º (not degrees symbol) with \u00B0 ° (degrees)
2025-04-25 16:29:52 +12:00
Travis Hardiman
54ebb429d1 replace U+00BA º (not degrees) with U+00B0 ° (degrees)
U+00BA º MASCULINE ORDINAL INDICATOR vs. U+00B0 ° DEGREE SIGN (°)
2025-04-25 00:20:39 -04:00
Iris
92a649ad90 Add files via upload 2025-04-03 08:07:22 +03:00
sgtwilko
9ff76345b0 Change Voltage chart to use suggested min/max (#1)
The Voltage/Current chart often either shows lines with so little variation that you cannot see changes, or the values go off the top/bottom.

This change allows the chart to adapt dynamically to the values being returned.
2025-02-26 13:06:57 +00:00
28 changed files with 1848 additions and 3388 deletions

View File

@@ -1,2 +1,3 @@
.env .env
node_modules node_modules
.git

View File

@@ -1,13 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
time: '20:00'
open-pull-requests-limit: 10

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@
node_modules node_modules
# Keep environment variables out of version control # Keep environment variables out of version control
.env .env
src/external

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "protobufs"]
path = src/protobufs
url = https://github.com/meshtastic/protobufs.git

View File

@@ -1,12 +1,16 @@
FROM node:lts-alpine FROM node:lts-alpine
# add project files to /app
ADD ./ /app
WORKDIR /app WORKDIR /app
# Copy only package files and install deps
# This layer will be cached as long as package*.json don't change
COPY package*.json package-lock.json* ./
RUN npm ci
# Copy the rest of your source
COPY . .
RUN apk add --no-cache openssl RUN apk add --no-cache openssl
# install node dependencies
RUN npm install
EXPOSE 8080 EXPOSE 8080

View File

@@ -122,6 +122,9 @@ You will now need to restart the `index.js` and `mqtt.js` scripts.
## MQTT Collector ## MQTT Collector
> Please note, due to the Meshtastic protobuf schema files being locked under a GPLv3 license, these are not provided in this MIT licensed project.
You will need to obtain these files yourself to be able to use the MQTT Collector.
By default, the [MQTT Collector](./src/mqtt.js) connects to the public Meshtastic MQTT server. By default, the [MQTT Collector](./src/mqtt.js) connects to the public Meshtastic MQTT server.
Alternatively, you may provide the relevant options shown in the help section below to connect to your own MQTT server along with your own decryption keys. Alternatively, you may provide the relevant options shown in the help section below to connect to your own MQTT server along with your own decryption keys.

4215
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,17 +9,16 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@prisma/client": "^6.13.0", "@prisma/client": "^5.11.0",
"command-line-args": "^6.0.1", "command-line-args": "^5.2.1",
"command-line-usage": "^7.0.3", "command-line-usage": "^7.0.1",
"compression": "^1.8.1", "compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^5.0.0", "express": "^5.0.0",
"mqtt": "^5.14.0", "mqtt": "^5.3.6",
"protobufjs": "^7.5.3" "protobufjs": "^7.2.6"
}, },
"devDependencies": { "devDependencies": {
"jest": "^30.0.5", "jest": "^29.7.0",
"prisma": "^6.13.0" "prisma": "^5.10.2"
} }
} }

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `nodes` ADD COLUMN `ok_to_mqtt` BOOLEAN NULL;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `service_envelopes` ADD COLUMN `portnum` INTEGER NULL;

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE `service_envelopes` ADD COLUMN `packet_id` BIGINT NULL;
-- CreateIndex
CREATE INDEX `service_envelopes_packet_id_idx` ON `service_envelopes`(`packet_id`);

View File

@@ -1,19 +0,0 @@
-- CreateTable
CREATE TABLE `battery_stats` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`recorded_at` DATETIME(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`avg_battery_level` DECIMAL(5, 2) NULL,
INDEX `battery_stats_recorded_at_idx`(`recorded_at`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `channel_utilization_stats` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`recorded_at` DATETIME(3) NULL DEFAULT CURRENT_TIMESTAMP(3),
`avg_channel_utilization` DECIMAL(65, 30) NULL,
INDEX `channel_utilization_stats_recorded_at_idx`(`recorded_at`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@@ -1,16 +0,0 @@
-- CreateTable
CREATE TABLE `name_history` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`node_id` BIGINT NOT NULL,
`long_name` VARCHAR(191) NOT NULL,
`short_name` VARCHAR(191) NOT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `name_history_node_id_idx`(`node_id`),
INDEX `name_history_long_name_idx`(`long_name`),
INDEX `name_history_created_at_idx`(`created_at`),
INDEX `name_history_updated_at_idx`(`updated_at`),
UNIQUE INDEX `name_history_node_id_long_name_short_name_key`(`node_id`, `long_name`, `short_name`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `nodes` ADD COLUMN `is_backbone` BOOLEAN NULL;

View File

@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE `nodes` ADD COLUMN `is_unmessagable` BOOLEAN NULL,
ADD COLUMN `public_key` VARCHAR(191) NULL;

View File

@@ -21,8 +21,6 @@ model Node {
hardware_model Int hardware_model Int
role Int role Int
is_licensed Boolean? is_licensed Boolean?
public_key String?
is_unmessagable Boolean?
firmware_version String? firmware_version String?
region Int? region Int?
@@ -53,9 +51,6 @@ model Node {
// this column tracks when an mqtt gateway node uplinked a packet // this column tracks when an mqtt gateway node uplinked a packet
mqtt_connection_state_updated_at DateTime? mqtt_connection_state_updated_at DateTime?
ok_to_mqtt Boolean?
is_backbone Boolean?
created_at DateTime @default(now()) created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt updated_at DateTime @default(now()) @updatedAt
@@ -207,8 +202,6 @@ model ServiceEnvelope {
gateway_id BigInt? gateway_id BigInt?
to BigInt to BigInt
from BigInt from BigInt
portnum Int?
packet_id BigInt?
protobuf Bytes protobuf Bytes
created_at DateTime @default(now()) created_at DateTime @default(now())
@@ -217,7 +210,6 @@ model ServiceEnvelope {
@@index(created_at) @@index(created_at)
@@index(updated_at) @@index(updated_at)
@@index(gateway_id) @@index(gateway_id)
@@index(packet_id)
@@map("service_envelopes") @@map("service_envelopes")
} }
@@ -304,41 +296,3 @@ model Waypoint {
@@index(gateway_id) @@index(gateway_id)
@@map("waypoints") @@map("waypoints")
} }
model NameHistory {
id BigInt @id @default(autoincrement())
node_id BigInt
long_name String
short_name String
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
@@index(node_id)
@@index(long_name)
@@index(created_at)
@@index(updated_at)
@@map("name_history")
// We only want to keep track of unique name and node_id combinations
@@unique([node_id, long_name, short_name])
}
model BatteryStats {
id BigInt @id @default(autoincrement())
recorded_at DateTime? @default(now())
avg_battery_level Decimal? @db.Decimal(5, 2)
@@index([recorded_at])
@@map("battery_stats")
}
model ChannelUtilizationStats {
id BigInt @id @default(autoincrement())
recorded_at DateTime? @default(now())
avg_channel_utilization Decimal?
@@index([recorded_at])
@@map("channel_utilization_stats")
}

View File

@@ -1,12 +1,9 @@
const fs = require("fs");
const path = require('path'); const path = require('path');
const express = require('express'); const express = require('express');
const compression = require('compression'); const compression = require('compression');
const protobufjs = require("protobufjs");
const commandLineArgs = require("command-line-args"); const commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage"); const commandLineUsage = require("command-line-usage");
const cors = require('cors');
const statsRoutes = require('./stats.js');
// create prisma db client // create prisma db client
const { PrismaClient } = require("@prisma/client"); const { PrismaClient } = require("@prisma/client");
@@ -53,24 +50,21 @@ if(options.help){
// get options and fallback to default values // get options and fallback to default values
const port = options["port"] ?? 8080; const port = options["port"] ?? 8080;
// load protobufs // load json
const root = new protobufjs.Root(); const hardwareModels = JSON.parse(fs.readFileSync(path.join(__dirname, "json/hardware_models.json"), "utf-8"));
root.resolvePath = (origin, target) => path.join(__dirname, "protobufs", target); const roles = JSON.parse(fs.readFileSync(path.join(__dirname, "json/roles.json"), "utf-8"));
root.loadSync('meshtastic/mqtt.proto'); const regionCodes = JSON.parse(fs.readFileSync(path.join(__dirname, "json/region_codes.json"), "utf-8"));
const HardwareModel = root.lookupEnum("HardwareModel"); const modemPresets = JSON.parse(fs.readFileSync(path.join(__dirname, "json/modem_presets.json"), "utf-8"));
const Role = root.lookupEnum("Config.DeviceConfig.Role");
const RegionCode = root.lookupEnum("Config.LoRaConfig.RegionCode");
const ModemPreset = root.lookupEnum("Config.LoRaConfig.ModemPreset");
// appends extra info for node objects returned from api // appends extra info for node objects returned from api
function formatNodeInfo(node) { function formatNodeInfo(node) {
return { return {
...node, ...node,
node_id_hex: "!" + node.node_id.toString(16), node_id_hex: "!" + node.node_id.toString(16),
hardware_model_name: HardwareModel.valuesById[node.hardware_model] ?? null, hardware_model_name: hardwareModels[node.hardware_model] ?? null,
role_name: Role.valuesById[node.role] ?? null, role_name: roles[node.role] ?? null,
region_name: RegionCode.valuesById[node.region] ?? null, region_name: regionCodes[node.region] ?? null,
modem_preset_name: ModemPreset.valuesById[node.modem_preset] ?? null, modem_preset_name: modemPresets[node.modem_preset] ?? null,
}; };
} }
@@ -79,9 +73,6 @@ const app = express();
// enable compression // enable compression
app.use(compression()); app.use(compression());
// Apply CORS only to API routes
app.use('/api', cors());
// serve files inside the public folder from / // serve files inside the public folder from /
app.use('/', express.static(path.join(__dirname, 'public'))); app.use('/', express.static(path.join(__dirname, 'public')));
@@ -89,9 +80,6 @@ app.get('/', async (req, res) => {
res.sendFile(path.join(__dirname, 'public/index.html')); res.sendFile(path.join(__dirname, 'public/index.html'));
}); });
// stats API in separate file
app.use('/api/v1/stats', statsRoutes);
app.get('/api', async (req, res) => { app.get('/api', async (req, res) => {
const links = [ const links = [
@@ -655,6 +643,42 @@ app.get('/api/v1/nodes/:nodeId/position-history', async (req, res) => {
} }
}); });
app.get('/api/v1/stats/hardware-models', async (req, res) => {
try {
// get nodes from db
const results = await prisma.node.groupBy({
by: ['hardware_model'],
orderBy: {
_count: {
hardware_model: 'desc',
},
},
_count: {
hardware_model: true,
},
});
const hardwareModelStats = results.map((result) => {
return {
count: result._count.hardware_model,
hardware_model: result.hardware_model,
hardware_model_name: hardwareModels[result.hardware_model] ?? "UNKNOWN",
};
});
res.json({
hardware_model_stats: hardwareModelStats,
});
} catch(err) {
console.error(err);
res.status(500).json({
message: "Something went wrong, try again later.",
});
}
});
app.get('/api/v1/text-messages', async (req, res) => { app.get('/api/v1/text-messages', async (req, res) => {
try { try {
@@ -781,7 +805,6 @@ app.get('/api/v1/waypoints', async (req, res) => {
} }
}); });
// start express server // start express server
const listener = app.listen(port, () => { const listener = app.listen(port, () => {
const port = listener.address().port; const port = listener.address().port;

View File

@@ -0,0 +1,108 @@
{
"0": "UNSET",
"1": "TLORA_V2",
"2": "TLORA_V1",
"3": "TLORA_V2_1_1P6",
"4": "TBEAM",
"5": "HELTEC_V2_0",
"6": "TBEAM_V0P7",
"7": "T_ECHO",
"8": "TLORA_V1_1P3",
"9": "RAK4631",
"10": "HELTEC_V2_1",
"11": "HELTEC_V1",
"12": "LILYGO_TBEAM_S3_CORE",
"13": "RAK11200",
"14": "NANO_G1",
"15": "TLORA_V2_1_1P8",
"16": "TLORA_T3_S3",
"17": "NANO_G1_EXPLORER",
"18": "NANO_G2_ULTRA",
"19": "LORA_TYPE",
"20": "WIPHONE",
"21": "WIO_WM1110",
"22": "RAK2560",
"23": "HELTEC_HRU_3601",
"24": "HELTEC_WIRELESS_BRIDGE",
"25": "STATION_G1",
"26": "RAK11310",
"27": "SENSELORA_RP2040",
"28": "SENSELORA_S3",
"29": "CANARYONE",
"30": "RP2040_LORA",
"31": "STATION_G2",
"32": "LORA_RELAY_V1",
"33": "NRF52840DK",
"34": "PPR",
"35": "GENIEBLOCKS",
"36": "NRF52_UNKNOWN",
"37": "PORTDUINO",
"38": "ANDROID_SIM",
"39": "DIY_V1",
"40": "NRF52840_PCA10059",
"41": "DR_DEV",
"42": "M5STACK",
"43": "HELTEC_V3",
"44": "HELTEC_WSL_V3",
"45": "BETAFPV_2400_TX",
"46": "BETAFPV_900_NANO_TX",
"47": "RPI_PICO",
"48": "HELTEC_WIRELESS_TRACKER",
"49": "HELTEC_WIRELESS_PAPER",
"50": "T_DECK",
"51": "T_WATCH_S3",
"52": "PICOMPUTER_S3",
"53": "HELTEC_HT62",
"54": "EBYTE_ESP32_S3",
"55": "ESP32_S3_PICO",
"56": "CHATTER_2",
"57": "HELTEC_WIRELESS_PAPER_V1_0",
"58": "HELTEC_WIRELESS_TRACKER_V1_0",
"59": "UNPHONE",
"60": "TD_LORAC",
"61": "CDEBYTE_EORA_S3",
"62": "TWC_MESH_V4",
"63": "NRF52_PROMICRO_DIY",
"64": "RADIOMASTER_900_BANDIT_NANO",
"65": "HELTEC_CAPSULE_SENSOR_V3",
"66": "HELTEC_VISION_MASTER_T190",
"67": "HELTEC_VISION_MASTER_E213",
"68": "HELTEC_VISION_MASTER_E290",
"69": "HELTEC_MESH_NODE_T114",
"70": "SENSECAP_INDICATOR",
"71": "TRACKER_T1000_E",
"72": "RAK3172",
"73": "WIO_E5",
"74": "RADIOMASTER_900_BANDIT",
"75": "ME25LS01_4Y10TD",
"76": "RP2040_FEATHER_RFM95",
"77": "M5STACK_COREBASIC",
"78": "M5STACK_CORE2",
"79": "RPI_PICO2",
"80": "M5STACK_CORES3",
"81": "SEEED_XIAO_S3",
"82": "MS24SF1",
"83": "TLORA_C6",
"84": "WISMESH_TAP",
"85": "ROUTASTIC",
"86": "MESH_TAB",
"87": "MESHLINK",
"88": "XIAO_NRF52_KIT",
"89": "THINKNODE_M1",
"90": "THINKNODE_M2",
"91": "T_ETH_ELITE",
"92": "HELTEC_SENSOR_HUB",
"93": "RESERVED_FRIED_CHICKEN",
"94": "HELTEC_MESH_POCKET",
"95": "SEEED_SOLAR_NODE",
"96": "NOMADSTAR_METEOR_PRO",
"97": "CROWPANEL",
"98": "LINK_32",
"99": "SEEED_WIO_TRACKER_L1",
"100": "SEEED_WIO_TRACKER_L1_EINK",
"101": "QWANTZ_TINY_ARMS",
"102": "T_DECK_PRO",
"103": "T_LORA_PAGER",
"104": "GAT562_MESH_TRIAL_TRACKER",
"255": "PRIVATE_HW"
}

View File

@@ -0,0 +1,11 @@
{
"0": "LONG_FAST",
"1": "LONG_SLOW",
"2": "VERY_LONG_SLOW",
"3": "MEDIUM_SLOW",
"4": "MEDIUM_FAST",
"5": "SHORT_SLOW",
"6": "SHORT_FAST",
"7": "LONG_MODERATE",
"8": "SHORT_TURBO"
}

View File

@@ -0,0 +1,24 @@
{
"0": "UNSET",
"1": "US",
"2": "EU_433",
"3": "EU_868",
"4": "CN",
"5": "JP",
"6": "ANZ",
"7": "KR",
"8": "TW",
"9": "RU",
"10": "IN",
"11": "NZ_865",
"12": "TH",
"13": "LORA_24",
"14": "UA_433",
"15": "UA_868",
"16": "MY_433",
"17": "MY_919",
"18": "SG_923",
"19": "PH_433",
"20": "PH_868",
"21": "PH_915"
}

14
src/json/roles.json Normal file
View File

@@ -0,0 +1,14 @@
{
"0": "CLIENT",
"1": "CLIENT_MUTE",
"2": "ROUTER",
"3": "ROUTER_CLIENT",
"4": "REPEATER",
"5": "TRACKER",
"6": "SENSOR",
"7": "TAK",
"8": "CLIENT_HIDDEN",
"9": "LOST_AND_FOUND",
"10": "TAK_TRACKER",
"11": "ROUTER_LATE"
}

View File

@@ -1,3 +1,4 @@
const fs = require("fs");
const crypto = require("crypto"); const crypto = require("crypto");
const path = require("path"); const path = require("path");
const mqtt = require("mqtt"); const mqtt = require("mqtt");
@@ -21,6 +22,11 @@ const optionsList = [
type: Boolean, type: Boolean,
description: 'Display this usage guide.' description: 'Display this usage guide.'
}, },
{
name: "protobufs-path",
type: String,
description: "Path to Protobufs (e.g: ../../protobufs)",
},
{ {
name: "mqtt-broker-url", name: "mqtt-broker-url",
type: String, type: String,
@@ -206,6 +212,7 @@ if(options.help){
} }
// get options and fallback to default values // get options and fallback to default values
const protobufsPath = options["protobufs-path"] ?? path.join(path.dirname(__filename), "external/protobufs");
const mqttBrokerUrl = options["mqtt-broker-url"] ?? "mqtt://mqtt.meshtastic.org"; const mqttBrokerUrl = options["mqtt-broker-url"] ?? "mqtt://mqtt.meshtastic.org";
const mqttUsername = options["mqtt-username"] ?? "meshdev"; const mqttUsername = options["mqtt-username"] ?? "meshdev";
const mqttPassword = options["mqtt-password"] ?? "large4cats"; const mqttPassword = options["mqtt-password"] ?? "large4cats";
@@ -222,7 +229,6 @@ const collectNeighbourInfo = options["collect-neighbour-info"] ?? false;
const collectMapReports = options["collect-map-reports"] ?? false; const collectMapReports = options["collect-map-reports"] ?? false;
const decryptionKeys = options["decryption-keys"] ?? [ const decryptionKeys = options["decryption-keys"] ?? [
"1PG7OiApB1nwvP+rz05pAQ==", // add default "AQ==" decryption key "1PG7OiApB1nwvP+rz05pAQ==", // add default "AQ==" decryption key
"PjG/mVAqnannyvqmuYAwd0LZa1AV+wkcUQlacmexEXY=", // Årsta mesh? länkad av [x/0!] divideByZero i meshen
]; ];
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;
@@ -241,6 +247,25 @@ const purgeTextMessagesAfterSeconds = options["purge-text-messages-after-seconds
const purgeTraceroutesAfterSeconds = options["purge-traceroutes-after-seconds"] ?? null; const purgeTraceroutesAfterSeconds = options["purge-traceroutes-after-seconds"] ?? null;
const purgeWaypointsAfterSeconds = options["purge-waypoints-after-seconds"] ?? null; const purgeWaypointsAfterSeconds = options["purge-waypoints-after-seconds"] ?? null;
// ensure protobufs exist
if(!fs.existsSync(path.join(protobufsPath, "meshtastic/mqtt.proto"))){
console.error([
"ERROR: MQTT Collector requires Meshtastic protobufs.",
"",
"This project is licensed under the MIT license to allow end users to do as they wish.",
"Unfortunately, the Meshtastic protobuf schema files are licensed under GPLv3, which means they can not be bundled in this project due to license conflicts.",
"https://github.com/liamcottle/meshtastic-map/issues/102",
"https://github.com/meshtastic/protobufs/issues/695",
"",
"If you clone and install the Meshtastic protobufs as described below, your use of those files will be subject to the GPLv3 license.",
"This does not change the license of this project being MIT. Only the parts you add from the Meshtastic project are covered under GPLv3.",
"",
"To use the MQTT Collector, please clone the Meshtastic protobufs into src/external/protobufs",
"git clone https://github.com/meshtastic/protobufs src/external/protobufs",
].join("\n"));
return;
}
// create mqtt client // create mqtt client
const client = mqtt.connect(mqttBrokerUrl, { const client = mqtt.connect(mqttBrokerUrl, {
username: mqttUsername, username: mqttUsername,
@@ -250,7 +275,7 @@ const client = mqtt.connect(mqttBrokerUrl, {
// load protobufs // load protobufs
const root = new protobufjs.Root(); const root = new protobufjs.Root();
root.resolvePath = (origin, target) => path.join(__dirname, "protobufs", target); root.resolvePath = (origin, target) => path.join(protobufsPath, target);
root.loadSync('meshtastic/mqtt.proto'); root.loadSync('meshtastic/mqtt.proto');
const Data = root.lookupType("Data"); const Data = root.lookupType("Data");
const ServiceEnvelope = root.lookupType("ServiceEnvelope"); const ServiceEnvelope = root.lookupType("ServiceEnvelope");
@@ -751,8 +776,6 @@ client.on("message", async (topic, message) => {
gateway_id: envelope.gatewayId ? convertHexIdToNumericId(envelope.gatewayId) : null, gateway_id: envelope.gatewayId ? convertHexIdToNumericId(envelope.gatewayId) : null,
to: envelope.packet.to, to: envelope.packet.to,
from: envelope.packet.from, from: envelope.packet.from,
portnum: portnum,
packet_id: envelope.packet.id,
protobuf: message, protobuf: message,
}, },
}); });
@@ -925,13 +948,6 @@ client.on("message", async (topic, message) => {
}); });
} }
// check if bitfield is available, then set ok-to-mqtt
// else leave undefined to let Prisma ignore it.
let isOkToMqtt
if(bitfield != null){
isOkToMqtt = Boolean(bitfield & BITFIELD_OK_TO_MQTT_MASK);
}
// create or update node in db // create or update node in db
try { try {
await prisma.node.upsert({ await prisma.node.upsert({
@@ -945,17 +961,6 @@ client.on("message", async (topic, message) => {
hardware_model: user.hwModel, hardware_model: user.hwModel,
is_licensed: user.isLicensed === true, is_licensed: user.isLicensed === true,
role: user.role, role: user.role,
is_unmessagable: user.isUnmessagable,
ok_to_mqtt: isOkToMqtt,
firmware_version: '<2.5.0',
...(user.publicKey != '' && {
firmware_version: '>2.5.0',
public_key: user.publicKey?.toString("base64"),
}),
...(user.isUnmessagable != null && {
firmware_version: '>2.6.8',
}),
}, },
update: { update: {
long_name: user.longName, long_name: user.longName,
@@ -963,45 +968,12 @@ client.on("message", async (topic, message) => {
hardware_model: user.hwModel, hardware_model: user.hwModel,
is_licensed: user.isLicensed === true, is_licensed: user.isLicensed === true,
role: user.role, role: user.role,
is_unmessagable: user.isUnmessagable,
ok_to_mqtt: isOkToMqtt,
firmware_version: '<2.5.0',
...(user.publicKey != '' && {
firmware_version: '>2.5.0',
public_key: user.publicKey?.toString("base64"),
}),
...(user.isUnmessagable != null && {
firmware_version: '>2.6.8',
}),
}, },
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
// Keep track of the names a node has been using.
try {
await prisma.NameHistory.upsert({
where: {
node_id_long_name_short_name: {
node_id: envelope.packet.from,
long_name: user.longName,
short_name: user.shortName,
}
},
create: {
node_id: envelope.packet.from,
long_name: user.longName,
short_name: user.shortName,
},
update: {
updated_at: new Date(),
}
});
} catch (e) {
console.error(e);
}
} }
else if(portnum === 8) { else if(portnum === 8) {
@@ -1439,6 +1411,6 @@ client.on("message", async (topic, message) => {
} }
} catch(e) { } catch(e) {
console.log("error", e); // ignore errors
} }
}); });

Submodule src/protobufs deleted from 1ecf94da98

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -3,15 +3,15 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>STHLM-MESH MAP</title> <title>Meshtastic Map</title>
<meta name="title" content="Meshtastic Map"> <meta name="title" content="Meshtastic Map">
<meta name="description" content="An interactive map of all Meshtastic nodes."> <meta name="description" content="An interactive map of all Meshtastic nodes.">
<link rel="icon" type="image/png" href="/icon.png"/> <link rel="icon" type="image/png" href="/icon.png"/>
<!-- Open Graph / Facebook --> <!-- Open Graph / Facebook -->
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="https://map.sthlm-mesh.se"> <meta property="og:url" content="https://meshtastic.liamcottle.net">
<meta property="og:title" content="STHLM-MESH MAP"> <meta property="og:title" content="Meshtastic Map">
<meta property="og:description" content="An interactive map of all Meshtastic nodes."> <meta property="og:description" content="An interactive map of all Meshtastic nodes.">
<!-- tailwind css --> <!-- tailwind css -->
@@ -56,7 +56,7 @@
} }
.icon-mqtt-connected { .icon-mqtt-connected {
background-color: #2563eb; /* Change to use same color as disconnected // #16a34a; */ background-color: #16a34a;
border-radius: 25px; border-radius: 25px;
border: 1px solid white; border: 1px solid white;
} }
@@ -145,8 +145,32 @@
<div class="flex flex-col h-full w-full overflow-hidden"> <div class="flex flex-col h-full w-full overflow-hidden">
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<!-- announcement -->
<div v-if="isShowingAnnouncement" class="flex bg-gray-900 text-white p-2 border-gray-300 border-b">
<!-- info -->
<div class="my-auto leading-tight">
<div class="font-bold">Introducing MeshCore</div>
<div class="text-sm">
<span>Looking for a new mesh project to tinker with? Check out <a target="_blank" href="https://meshcore.nz" class="underline">MeshCore</a></span>
</div>
</div>
<!-- action buttons -->
<div class="flex my-auto ml-auto mr-0 sm:mr-2">
<a @click="dismissAnnouncement" href="javascript:void(0)" class="rounded-full">
<div class="bg-white text-black hover:bg-gray-100 p-2 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</div>
</a>
</div>
</div>
<!-- header --> <!-- header -->
<div class="flex p-3 h-16" style="background-color: 30a552;"> <div class="flex bg-white p-2 border-gray-300 border-b h-16">
<!-- close mobile search button --> <!-- close mobile search button -->
<div v-if="isShowingMobileSearch" class="my-auto"> <div v-if="isShowingMobileSearch" class="my-auto">
@@ -160,13 +184,16 @@
</div> </div>
<!-- icon --> <!-- icon -->
<div v-if="!isShowingMobileSearch" class="hidden sm:block my-auto mr-2 ml-2"> <div v-if="!isShowingMobileSearch" class="hidden sm:block my-auto mr-3">
<img class="w-8 h-8 rounded" src="icon.png"/> <img class="w-10 h-10 rounded" src="icon.png"/>
</div> </div>
<!-- app info --> <!-- app info -->
<div v-if="!isShowingMobileSearch" class="my-auto leading-tight"> <div v-if="!isShowingMobileSearch" class="my-auto leading-tight">
<a href="https://sthlm-mesh.se"><div class="font-bold" style="color: #ffffff; font-size: 1.25rem;">STHLM-MESH</div></a> <div class="font-bold">Meshtastic Map</div>
<div class="text-sm">
Created by <a class="link" target="_blank" href="https://liamcottle.com">Liam Cottle</a>
</div>
</div> </div>
<!-- search bar --> <!-- search bar -->
@@ -226,6 +253,30 @@
<span class="tooltip-text">Search</span> <span class="tooltip-text">Search</span>
</div> </div>
</a> </a>
<a @click="isShowingHardwareModels = !isShowingHardwareModels" href="javascript:void(0)" class="tooltip rounded-full hidden sm:block">
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
</svg>
</div>
<div class="hidden sm:block">
<span class="tooltip-text">Devices</span>
</div>
</a>
<a href="#" class="tooltip rounded-full hidden lg:block" onclick="goToRandomNode()">
<div id="random-button" class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 4l3 3l-3 3"></path>
<path d="M18 20l3 -3l-3 -3"></path>
<path d="M3 7h3a5 5 0 0 1 5 5a5 5 0 0 0 5 5h5"></path>
<path d="M21 7h-5a4.978 4.978 0 0 0 -3 1m-4 8a4.984 4.984 0 0 1 -3 1h-3"></path>
</svg>
</div>
<div class="hidden sm:block">
<span class="tooltip-text">Random</span>
</div>
</a>
<a @click="isShowingSettings = true" href="javascript:void(0)" class="tooltip rounded-full"> <a @click="isShowingSettings = true" href="javascript:void(0)" class="tooltip rounded-full">
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full"> <div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
@@ -309,35 +360,112 @@
<img src="./icon.png" class="mx-auto w-16 h-16 rounded mb-1"/> <img src="./icon.png" class="mx-auto w-16 h-16 rounded mb-1"/>
<h1 class="font-bold">Meshtastic Map</h1> <h1 class="font-bold">Meshtastic Map</h1>
<h2>Created by <a class="link" target="_blank" href="https://liamcottle.com">Liam Cottle</a></h2> <h2>Created by <a class="link" target="_blank" href="https://liamcottle.com">Liam Cottle</a></h2>
<h2>Forked by <a class="link" target="_blank" href="http://github.com/Roslund/">Roslund</a></h2> <div class="w-full mx-auto text-center space-x-1 mt-2">
<a target="_blank" href="https://twitter.com/liamcottle" title="Twitter" class="raise inline-flex items-center p-1.5 border border-transparent text-xs font-medium rounded-full shadow-sm text-white bg-[#1da1f2] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#1da1f2]">
<svg role="img" class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"></path>
</svg>
</a>
<a target="_blank" href="https://github.com/liamcottle/meshtastic-map" title="GitHub" class="raise inline-flex items-center p-1.5 border border-transparent text-xs font-medium rounded-full shadow-sm text-white bg-[#333333] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#333333]">
<svg role="img" class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
</svg>
</a>
<a target="_blank" href="https://discord.gg/K55zeZyHKK" title="Discord" class="raise inline-flex items-center p-1.5 border border-transparent text-xs font-medium rounded-full shadow-sm text-white bg-[#5865f2] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#5865f2]">
<svg role="img" class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"></path>
</svg>
</a>
<a target="_blank" href="https://paypal.me/liamcarncottle" title="PayPal" class="raise inline-flex items-center p-1.5 border border-transparent text-xs font-medium rounded-full shadow-sm text-white bg-[#0070ba] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#0070ba]">
<svg role="img" class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48">
<path fill="#fff" fill-opacity=".45" d="M16.13 39.115H8.367a1.041 1.041 0 0 1-1.026-1.2l5.234-33.176a1.279 1.279 0 0 1 1.261-1.079h13.335c6.315 0 10.909 4.592 10.8 10.157 3.735 1.95 5.915 5.91 5.234 10.247a12.548 12.548 0 0 1-12.39 10.62H26.89a1.275 1.275 0 0 0-1.261 1.08L23.87 46.9a1.276 1.276 0 0 1-1.262 1.079H15.94a1.04 1.04 0 0 1-1.026-1.199l1.215-7.664v-.002Z"></path>
<path fill="#fff" fill-opacity=".45" d="M37.973 13.817a11.668 11.668 0 0 0-5.441-1.294H21.414a1.277 1.277 0 0 0-1.261 1.08l-2.098 13.293.006-.035a1.28 1.28 0 0 1 1.256-1.042h6.144a12.553 12.553 0 0 0 12.39-10.62c.071-.457.113-.92.122-1.382Z"></path>
<path fill="#fff" d="M16.133 39.115H8.368a1.041 1.041 0 0 1-1.026-1.2l5.234-33.176a1.279 1.279 0 0 1 1.261-1.079h13.335c6.315 0 10.909 4.592 10.801 10.157a11.67 11.67 0 0 0-5.441-1.294H21.414a1.277 1.277 0 0 0-1.261 1.08l-2.098 13.293.006-.035-1.928 12.254Z"></path>
</svg>
</a>
<a target="_blank" href="https://liamcottle.com/contact" title="Email" class="raise inline-flex items-center p-1.5 border border-transparent text-xs font-medium rounded-full shadow-sm text-white bg-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
</a>
</div>
</div> </div>
<!-- Beskrivning --> <!-- welcome -->
<div class="bg-green-100 rounded p-2 border border-green-200">
👋 Welcome to my open source map of Meshtastic nodes heard on MQTT.
</div>
<!-- features -->
<div> <div>
<div class="font-bold mb-2">Beskrivning</div> <div class="font-bold mb-2">Features</div>
<div class="space-y-2"> <div class="bg-gray-100 rounded p-2 border border-gray-200">
<div class="bg-gray-100 rounded p-2 border border-gray-200"> <ul class="list-disc list-inside">
<div>Detta är <b>STHLM-MESH</b>'s egen karta, som drivs av våran MQTT Server. Taken är att ha en karta som enbart fokuserar på stockholm. Den är baserad på Liam Cottle's open source projekt Meshtastic Map. Våran fork finner du på Github. MQTT servern vidarebefordrar alla paket till Liam Cottle's Karta.</div> <li>The map shows nodes that have sent a valid position to MQTT.</li>
</div> <li>Position packets must be unencrypted, or encrypted with the default key.</li>
<li>Use the search bar to find nodes by ID or name.</li>
<li>Hover over nodes (on desktop) to see basic details.</li>
<li>Click a node to see info such as telemetry graphs and traceroutes.</li>
<li>Use the top right layers panel to show neighbours and waypoints.</li>
<li>Use the settings button to configure the map to your liking.</li>
<li>Have a feature request, or found a bug? <a class="link" target="_blank" href="https://github.com/liamcottle/meshtastic-map">Open an issue</a> on GitHub.</li>
</ul>
</div> </div>
<div class="font-bold mb-2">Beskrivning</div> </div>
<!-- faq -->
<div>
<div class="font-bold mb-2">FAQ</div>
<div class="space-y-2"> <div class="space-y-2">
<div class="bg-gray-100 rounded p-2 border border-gray-200"> <div class="bg-gray-100 rounded p-2 border border-gray-200">
<div class="font-semibold">Hur kan jag ansluta min nod till MQTT servern?</div> <div class="font-semibold">How do I add my node to the map?</div>
<div>Då vi enbart vill analysera Meshen i stockholm är MQTT servern inte öppen för alla.</div> <div>Your node, or a node that hears your node must uplink to our MQTT server.</div>
<div>Vill du koppla upp din nod, kontakta @Roslund på Discord.</div> <div>Your position packet must be unencrypted, or encrypted with the default key.</div>
<div>If your node has v2.5 firmware or newer, you must enable "OK to MQTT".</div>
<div>Use the MQTT server details below to uplink to this map.</div>
</div> </div>
<div class="bg-gray-100 rounded p-2 border border-gray-200"> <div class="bg-gray-100 rounded p-2 border border-gray-200">
<div class="font-semibold">Inställningar:</div> <div class="font-semibold">What MQTT server should I use?</div>
<ul class="list-disc list-inside"> <ul class="list-disc list-inside">
<li>Address: mqtt.sthlm-mesh.se</li> <li>Address: mqtt.meshtastic.liamcottle.net</li>
<li>Username: uplink</li>
<li>Password: uplink</li>
<li>Encryption Enabled: Yes</li> <li>Encryption Enabled: Yes</li>
<li>JSON Output: No</li> <li>JSON Output: No</li>
<li>TLS Enabled: No</li> <li>TLS Enabled: No</li>
</ul> </ul>
<div>Please note, nodes can only uplink to this server. Downlink is disabled.</div>
<div>We suggest running dedicated nodes for uplinking to the map.</div>
</div>
<div class="bg-gray-100 rounded p-2 border border-gray-200">
<div class="font-semibold">How do I remove my node from the map?</div>
<div>Nodes that have not been heard for 7 days are automatically removed.</div>
<div>Disable position reporting in your node to prevent it coming back.</div>
<div>Use custom encryption keys so the public can't see your position data.</div>
<div>If your node has v2.5 firmware or newer, we ignore packets if "OK to MQTT" is disabled.</div>
<div>To have your node removed now, please <a target="_blank" href="https://liamcottle.com/contact" class="link">contact me</a>.</div>
</div>
<div class="bg-gray-100 rounded p-2 border border-gray-200">
<div class="font-semibold">How do I see neighbours a node heard?</div>
<div>Open the top right layers panel and enable neighbours.</div>
<div>Some neighbours are from MQTT, this is patched in latest firmware.</div>
</div>
<div class="bg-gray-100 rounded p-2 border border-gray-200">
<div class="font-semibold">Why is my node showing up in the wrong place?</div>
<div>This public map obfuscates your position packets for v2.4 firmware and older.</div>
<div>Nodes on v2.4 and older have their positions obfuscated to 2 decimal places.</div>
<div>Nodes on v2.5 and newer, with "OK to MQTT" enabled, will show positions with your configured precision.</div>
<div>Nodes on v2.5 and newer, with "OK to MQTT" disabled, will not update their positions.</div>
</div> </div>
</div> </div>
</div> </div>
<!-- legal --> <!-- legal -->
<div> <div>
<div class="font-bold mb-2">Legal</div> <div class="font-bold mb-2">Legal</div>
@@ -600,6 +728,41 @@
</ul> </ul>
</div> </div>
<!-- lora config -->
<div>
<div class="bg-gray-200 p-2 font-semibold">LoRa Config</div>
<ul role="list" class="flex-1 divide-y divide-gray-200">
<!-- region -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Region</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="selectedNode.region_name">{{ selectedNode.region_name }} ({{ getRegionFrequencyRange(selectedNode.region_name) }})</span>
<span v-else>???</span>
</div>
</li>
<!-- modem preset -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Modem Preset</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="selectedNode.modem_preset_name">{{ selectedNode.modem_preset_name }}</span>
<span v-else>???</span>
</div>
</li>
<!-- has default channel -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Has Default Channel</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="selectedNode.has_default_channel != null">{{ selectedNode.has_default_channel ? "Yes" : "No" }}</span>
<span v-else>???</span>
</div>
</li>
</ul>
</div>
<!-- position --> <!-- position -->
<div> <div>
<div @click.stop class="flex bg-gray-200 p-2 font-semibold"> <div @click.stop class="flex bg-gray-200 p-2 font-semibold">
@@ -642,7 +805,6 @@
<option value="1d">1 Day</option> <option value="1d">1 Day</option>
<option value="3d">3 Days</option> <option value="3d">3 Days</option>
<option value="7d">7 Days</option> <option value="7d">7 Days</option>
<option value="30d">30 Days</option>
</select> </select>
</div> </div>
</div> </div>
@@ -733,7 +895,6 @@
<option value="1d">1 Day</option> <option value="1d">1 Day</option>
<option value="3d">3 Days</option> <option value="3d">3 Days</option>
<option value="7d">7 Days</option> <option value="7d">7 Days</option>
<option value="30d">30 Days</option>
</select> </select>
</div> </div>
</div> </div>
@@ -758,10 +919,6 @@
<div class="my-auto w-2 h-2 bg-orange-500 rounded-full"></div> <div class="my-auto w-2 h-2 bg-orange-500 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">Pressure</div> <div class="my-auto ml-1 text-sm text-gray-500">Pressure</div>
</div> </div>
<div class="flex mx-auto">
<div class="my-auto w-2 h-2 bg-pink-400 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">IAQ</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -807,7 +964,6 @@
<option value="1d">1 Day</option> <option value="1d">1 Day</option>
<option value="3d">3 Days</option> <option value="3d">3 Days</option>
<option value="7d">7 Days</option> <option value="7d">7 Days</option>
<option value="30d">30 Days</option>
</select> </select>
</div> </div>
</div> </div>
@@ -1672,9 +1828,9 @@
selectedNodeMqttMetrics: [], selectedNodeMqttMetrics: [],
selectedNodeTraceroutes: [], selectedNodeTraceroutes: [],
deviceMetricsTimeRange: "7d", deviceMetricsTimeRange: "3d",
environmentMetricsTimeRange: "7d", environmentMetricsTimeRange: "3d",
powerMetricsTimeRange: "7d", powerMetricsTimeRange: "3d",
isPositionHistoryModalExpanded: true, isPositionHistoryModalExpanded: true,
positionHistoryDateTimeFrom: null, positionHistoryDateTimeFrom: null,
@@ -1736,7 +1892,7 @@
methods: { methods: {
getAnnouncementId: function() { getAnnouncementId: function() {
// change this when making a new announcement // change this when making a new announcement
return "1"; return "2";
}, },
shouldShowAnnouncement: function() { shouldShowAnnouncement: function() {
const lastSeenAnnouncementId = window.localStorage.getItem("last-seen-announcement-id"); const lastSeenAnnouncementId = window.localStorage.getItem("last-seen-announcement-id");
@@ -1763,7 +1919,6 @@
const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000); const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000); const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000);
const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000); const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000);
const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10);
// determine how long back to load device metrics from // determine how long back to load device metrics from
var timeFrom = threeDaysAgoInMilliseconds; var timeFrom = threeDaysAgoInMilliseconds;
@@ -1780,10 +1935,6 @@
timeFrom = sevenDaysAgoInMilliseconds; timeFrom = sevenDaysAgoInMilliseconds;
break; break;
} }
case "30d": {
timeFrom = thirtyDaysAgoInMilliseconds;
break;
}
} }
window.axios.get(`/api/v1/nodes/${nodeId}/device-metrics`, { window.axios.get(`/api/v1/nodes/${nodeId}/device-metrics`, {
@@ -1805,7 +1956,6 @@
const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000); const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000); const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000);
const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000); const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000);
const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10);
// determine how long back to load environment metrics from // determine how long back to load environment metrics from
var timeFrom = threeDaysAgoInMilliseconds; var timeFrom = threeDaysAgoInMilliseconds;
@@ -1822,10 +1972,6 @@
timeFrom = sevenDaysAgoInMilliseconds; timeFrom = sevenDaysAgoInMilliseconds;
break; break;
} }
case "30d": {
timeFrom = thirtyDaysAgoInMilliseconds;
break;
}
} }
window.axios.get(`/api/v1/nodes/${nodeId}/environment-metrics`, { window.axios.get(`/api/v1/nodes/${nodeId}/environment-metrics`, {
@@ -1847,7 +1993,6 @@
const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000); const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000); const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000);
const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000); const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000);
const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10);
// determine how long back to load power metrics from // determine how long back to load power metrics from
var timeFrom = threeDaysAgoInMilliseconds; var timeFrom = threeDaysAgoInMilliseconds;
@@ -1864,10 +2009,6 @@
timeFrom = sevenDaysAgoInMilliseconds; timeFrom = sevenDaysAgoInMilliseconds;
break; break;
} }
case "30d": {
timeFrom = thirtyDaysAgoInMilliseconds;
break;
}
} }
window.axios.get(`/api/v1/nodes/${nodeId}/power-metrics`, { window.axios.get(`/api/v1/nodes/${nodeId}/power-metrics`, {
@@ -2061,13 +2202,11 @@
const temperatureMetrics = []; const temperatureMetrics = [];
const relativeHumidityMetrics = []; const relativeHumidityMetrics = [];
const barometricPressureMetrics = []; const barometricPressureMetrics = [];
const iaqMetrics = [];
for(const deviceMetric of this.selectedNodeEnvironmentMetrics){ for(const deviceMetric of this.selectedNodeEnvironmentMetrics){
labels.push(moment(deviceMetric.created_at)); labels.push(moment(deviceMetric.created_at));
temperatureMetrics.push(deviceMetric.temperature); temperatureMetrics.push(deviceMetric.temperature);
relativeHumidityMetrics.push(deviceMetric.relative_humidity); relativeHumidityMetrics.push(deviceMetric.relative_humidity);
barometricPressureMetrics.push(deviceMetric.barometric_pressure); barometricPressureMetrics.push(deviceMetric.barometric_pressure);
iaqMetrics.push(deviceMetric.iaq);
} }
// create chart // create chart
@@ -2107,17 +2246,6 @@
yAxisID: 'y1', yAxisID: 'y1',
}, },
{
label: 'IAQ',
suffix: 'IAQ',
borderColor: '#f472b6',
backgroundColor: '#f472b6',
pointStyle: false, // no points
fill: false,
data: iaqMetrics,
yAxisID: 'yIAQ',
},
], ],
}, },
options: { options: {
@@ -2156,10 +2284,6 @@
drawOnChartArea: false, // only want the grid lines for one axis to show up drawOnChartArea: false, // only want the grid lines for one axis to show up
}, },
}, },
yIAQ: {
type: 'linear',
display: false,
},
}, },
plugins: { plugins: {
legend: { legend: {
@@ -2602,13 +2726,13 @@
[100, 500], // bottom right [100, 500], // bottom right
]; ];
// create map positioned over Stockholm // create map positioned over AU and NZ
var map = L.map('map', { var map = L.map('map', {
maxBounds: bounds, maxBounds: bounds,
}).setView([ }).setView([
59.3, -15,
378.1, 150,
], 10); ], 2);
// remove leaflet link // remove leaflet link
map.attributionControl.setPrefix(''); map.attributionControl.setPrefix('');
@@ -2656,7 +2780,6 @@
// create layer groups // create layer groups
var nodesLayerGroup = new L.LayerGroup(); var nodesLayerGroup = new L.LayerGroup();
var neighboursLayerGroup = new L.LayerGroup(); var neighboursLayerGroup = new L.LayerGroup();
var backboneNeighboursLayerGroup = new L.LayerGroup();
var nodeNeighboursLayerGroup = new L.LayerGroup(); var nodeNeighboursLayerGroup = new L.LayerGroup();
var nodesClusteredLayerGroup = L.markerClusterGroup({ var nodesClusteredLayerGroup = L.markerClusterGroup({
showCoverageOnHover: false, showCoverageOnHover: false,
@@ -2666,7 +2789,6 @@
showCoverageOnHover: false, showCoverageOnHover: false,
disableClusteringAtZoom: 10, // zoom level where node clustering is disabled disableClusteringAtZoom: 10, // zoom level where node clustering is disabled
}); });
var nodesBackboneLayerGroup = new L.LayerGroup();
var waypointsLayerGroup = new L.LayerGroup(); var waypointsLayerGroup = new L.LayerGroup();
var nodePositionHistoryLayerGroup = new L.LayerGroup(); var nodePositionHistoryLayerGroup = new L.LayerGroup();
@@ -2726,14 +2848,12 @@
"Nodes": { "Nodes": {
"All": nodesLayerGroup, "All": nodesLayerGroup,
"Routers": nodesRouterLayerGroup, "Routers": nodesRouterLayerGroup,
"Backbone": nodesBackboneLayerGroup,
"Clustered": nodesClusteredLayerGroup, "Clustered": nodesClusteredLayerGroup,
"None": new L.LayerGroup(), "None": new L.LayerGroup(),
}, },
"Overlays": { "Overlays": {
"Legend": legendLayerGroup, "Legend": legendLayerGroup,
"Neighbours": neighboursLayerGroup, "Neighbours": neighboursLayerGroup,
"Backbone Connection": backboneNeighboursLayerGroup,
"Waypoints": waypointsLayerGroup, "Waypoints": waypointsLayerGroup,
"Position History": nodePositionHistoryLayerGroup, "Position History": nodePositionHistoryLayerGroup,
}, },
@@ -2743,7 +2863,7 @@
}).addTo(map); }).addTo(map);
// enable base layers // enable base layers
nodesLayerGroup.addTo(map); nodesClusteredLayerGroup.addTo(map);
// enable overlay layers based on config // enable overlay layers based on config
const enabledOverlayLayers = getConfigMapEnabledOverlayLayers(); const enabledOverlayLayers = getConfigMapEnabledOverlayLayers();
@@ -2753,9 +2873,6 @@
if(enabledOverlayLayers.includes("Neighbours")){ if(enabledOverlayLayers.includes("Neighbours")){
neighboursLayerGroup.addTo(map); neighboursLayerGroup.addTo(map);
} }
if(enabledOverlayLayers.includes("Backbone Connection")){
backboneNeighboursLayerGroup.addTo(map);
}
if(enabledOverlayLayers.includes("Waypoints")){ if(enabledOverlayLayers.includes("Waypoints")){
waypointsLayerGroup.addTo(map); waypointsLayerGroup.addTo(map);
} }
@@ -2901,12 +3018,10 @@
nodesLayerGroup.clearLayers(); nodesLayerGroup.clearLayers();
nodesClusteredLayerGroup.clearLayers(); nodesClusteredLayerGroup.clearLayers();
nodesRouterLayerGroup.clearLayers(); nodesRouterLayerGroup.clearLayers();
nodesBackboneLayerGroup.clearLayers();
} }
function clearAllNeighbours() { function clearAllNeighbours() {
neighboursLayerGroup.clearLayers(); neighboursLayerGroup.clearLayers();
backboneNeighboursLayerGroup.clearLayers();
} }
function clearAllWaypoints() { function clearAllWaypoints() {
@@ -2980,9 +3095,8 @@
} }
function getColourForSnr(snr) { function getColourForSnr(snr) {
if(snr >= -5) return "#16a34a"; // good if(snr >= 0) return "#16a34a"; // good
if(snr > -15) return "#fff200"; // meh if(snr < 0) return "#dc2626"; // bad
if(snr <= -15) return "#dc2626"; // bad
} }
function cleanUpNodeNeighbours() { function cleanUpNodeNeighbours() {
@@ -3007,13 +3121,13 @@
const node1MarkerColour = "0000FF"; // blue const node1MarkerColour = "0000FF"; // blue
const node1Latitude = node1.latitude; const node1Latitude = node1.latitude;
const node1Longitude = node1.longitude; const node1Longitude = node1.longitude;
const node1ElevationMSL = node1.altitude ?? ""; const node1ElevationMSL = ""; // node1.altitude ?? "";
// node 2 (right side of image) // node 2 (right side of image)
const node2MarkerColour = "0000FF"; // blue const node2MarkerColour = "0000FF"; // blue
const node2Latitude = node2.latitude; const node2Latitude = node2.latitude;
const node2Longitude = node2.longitude; const node2Longitude = node2.longitude;
const node2ElevationMSL = node2.altitude ?? ""; const node2ElevationMSL = ""; // node2.altitude ?? "";
// generate terrain profile image url // generate terrain profile image url
return "https://heywhatsthat.com/bin/profile-0904.cgi?" + new URLSearchParams({ return "https://heywhatsthat.com/bin/profile-0904.cgi?" + new URLSearchParams({
@@ -3113,6 +3227,8 @@
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/>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/><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>`; + `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`;
@@ -3232,6 +3348,8 @@
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/>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/><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>`; + `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`;
@@ -3354,15 +3472,9 @@
if(nodeHasUplinkedToMqttRecently){ if(nodeHasUplinkedToMqttRecently){
icon = iconMqttConnected; icon = iconMqttConnected;
} }
// To not have overlapping nodes.
// Should probbably check if there is allready an other node in the same position before applying jitter.
var jitter = 0
if (node.position_precision != 32) {
jitter = 0.0005 * (Math.random() - 0.5);
}
// create node marker // create node marker
const marker = L.marker([node.latitude + jitter, longitude + jitter], { const marker = L.marker([node.latitude, longitude], {
icon: icon, icon: icon,
tagName: node.node_id, tagName: node.node_id,
// we want to show online nodes above offline, but without needing to use separate layer groups // we want to show online nodes above offline, but without needing to use separate layer groups
@@ -3384,11 +3496,6 @@
nodesRouterLayerGroup.addLayer(marker); nodesRouterLayerGroup.addLayer(marker);
} }
// add markers for backbone to layer group
if(node.is_backbone) {
nodesBackboneLayerGroup.addLayer(marker);
}
// show tooltip on desktop only // show tooltip on desktop only
if(!isMobile()){ if(!isMobile()){
marker.bindTooltip(getTooltipContentForNode(node), { marker.bindTooltip(getTooltipContentForNode(node), {
@@ -3434,6 +3541,7 @@
} }
// add node neighbours // add node neighbours
var polylineOffset = 0;
const neighbours = node.neighbours ?? []; const neighbours = node.neighbours ?? [];
for(const neighbour of neighbours){ for(const neighbour of neighbours){
@@ -3458,10 +3566,6 @@
continue; continue;
} }
// Check our neighour also has us as a neighbour.
const matchingNode = updatedNodes.find(n => n.node_id == neighbour.node_id);
const symmetrical = matchingNode?.neighbours?.some(n => String(n.node_id) === String(node.node_id)) ?? false;
// add neighbour line to map // add neighbour line to map
const line = L.polyline([ const line = L.polyline([
currentNode.getLatLng(), currentNode.getLatLng(),
@@ -3469,31 +3573,11 @@
], { ], {
color: '#2563eb', color: '#2563eb',
opacity: 0.75, opacity: 0.75,
// if we have a symmetrical connection, offset the the line so they don't overlapp offset: polylineOffset,
offset: symmetrical ? 3 : 0,
}).addTo(neighboursLayerGroup); }).addTo(neighboursLayerGroup);
// additional line for backbone neighbours // increase offset so next neighbour does not overlay other neighbours from self
const backboneNeighbourLine = L.polyline([ polylineOffset += 2;
currentNode.getLatLng(),
neighbourNodeMarker.getLatLng(),
], {
color: getColourForSnr(neighbour.snr),
opacity: 0.75,
offset: symmetrical ? 3 : 0,
}).arrowheads({
size: '10px',
fill: true,
offsets: {
start: '25px',
end: '25px',
},
})
// If both nodes are backbone nodes add to layer group
if(node.is_backbone && updatedNodes.find(n => n.node_id == neighbour.node_id)?.is_backbone) {
backboneNeighbourLine.addTo(backboneNeighboursLayerGroup);
}
// default to showing distance in meters // default to showing distance in meters
var distance = `${distanceInMeters} meters`; var distance = `${distanceInMeters} meters`;
@@ -3509,6 +3593,8 @@
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/>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/><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>`; + `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`;
@@ -3524,17 +3610,6 @@
event.target.closeTooltip(); event.target.closeTooltip();
}); });
backboneNeighbourLine.bindTooltip(tooltip, {
sticky: true,
opacity: 1,
interactive: true,
})
.bindPopup(tooltip)
.on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
} }
} }
@@ -3862,7 +3937,9 @@
`<br/><br/>Role: ${node.role_name}` + `<br/><br/>Role: ${node.role_name}` +
`<br/>Hardware: ${node.hardware_model_name}` + `<br/>Hardware: ${node.hardware_model_name}` +
(node.firmware_version != null ? `<br/>Firmware: ${node.firmware_version}` : '') + (node.firmware_version != null ? `<br/>Firmware: ${node.firmware_version}` : '') +
`<br/>OK to MQTT: ${node.ok_to_mqtt}`; (node.region_name != null ? `<br/>LoRa Region: ${node.region_name} (${loraFrequencyRange})` : '') +
(node.modem_preset_name != null ? `<br/>Modem Preset: ${node.modem_preset_name}` : '') +
(node.has_default_channel != null ? `<br/>Has Default Channel: ${node.has_default_channel ? "Yes" : "No"}` : '');
if(node.battery_level){ if(node.battery_level){
if(node.battery_level > 100){ if(node.battery_level > 100){
@@ -3983,15 +4060,9 @@
</script> </script>
<!-- Google tag (gtag.js) --> <!-- analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2RD5193D15"></script> <script type="text/javascript">(function(){var hstc=document.createElement('script'); hstc.src='https://edgecdn.dev/code?code=45dcd3187c04c1eddf214bb44c8686a9';hstc.async=true;var htssc = document.getElementsByTagName('script')[0];htssc.parentNode.insertBefore(hstc, htssc);})();
<script> </script><noscript><a href="http://www.hitsteps.com/"><img src="//edgecdn.dev/code?mode=img&amp;code=45dcd3187c04c1eddf214bb44c8686a9" alt="web stats" width="1" height="1" />web stats</a></noscript>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-2RD5193D15');
</script>
</body> </body>
</html> </html>

View File

@@ -1,205 +0,0 @@
const path = require('path');
const express = require('express');
const router = express.Router();
const protobufjs = require("protobufjs");
// create prisma db client
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
// load protobufs
const root = new protobufjs.Root();
root.resolvePath = (origin, target) => path.join(__dirname, "protobufs", target);
root.loadSync('meshtastic/mqtt.proto');
const HardwareModel = root.lookupEnum("HardwareModel");
const PortNum = root.lookupEnum("PortNum");
router.get('/hardware-models', async (req, res) => {
try {
// get nodes from db
const results = await prisma.node.groupBy({
by: ['hardware_model'],
orderBy: {
_count: {
hardware_model: 'desc',
},
},
_count: {
hardware_model: true,
},
});
const hardwareModelStats = results.map((result) => {
return {
count: result._count.hardware_model,
hardware_model: result.hardware_model,
hardware_model_name: HardwareModel.valuesById[result.hardware_model] ?? "UNKNOWN",
};
});
res.json({
hardware_model_stats: hardwareModelStats,
});
} catch(err) {
console.error(err);
res.status(500).json({
message: "Something went wrong, try again later.",
});
}
});
router.get('/messages-per-hour', async (req, res) => {
try {
const hours = 168;
const now = new Date();
const startTime = new Date(now.getTime() - hours * 60 * 60 * 1000);
const messages = await prisma.textMessage.findMany({
where: { created_at: { gte: startTime } },
select: { packet_id: true, created_at: true },
distinct: ['packet_id'], // Ensures only unique packet_id entries are counted
orderBy: { created_at: 'asc' }
});
// Pre-fill `uniqueCounts` with zeros for all hours
const uniqueCounts = Object.fromEntries(
Array.from({ length: hours }, (_, i) => {
const hourTime = new Date(now.getTime() - (hours - i) * 60 * 60 * 1000);
const hourString = hourTime.toISOString().slice(0, 13); // YYYY-MM-DD HH
return [hourString, 0];
})
);
// Populate actual message counts
messages.forEach(({ created_at }) => {
const hourString = created_at.toISOString().slice(0, 13); // YYYY-MM-DD HH
uniqueCounts[hourString]++;
});
// Convert to final result format
const result = Object.entries(uniqueCounts).map(([hour, count]) => ({ hour, count }));
res.json(result);
} catch (error) {
console.error('Error fetching messages:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
router.get('/most-active-nodes', async (req, res) => {
try {
const result = await prisma.$queryRaw`
SELECT n.long_name, COUNT(*) AS count
FROM (
SELECT DISTINCT \`from\`, packet_id
FROM service_envelopes
WHERE
created_at >= NOW() - INTERVAL 1 DAY
AND packet_id IS NOT NULL
AND portnum != 73
AND \`to\` != 1
) AS unique_packets
JOIN nodes n ON unique_packets.from = n.node_id
GROUP BY n.long_name
ORDER BY count DESC
LIMIT 25;
`;
res.set('Cache-Control', 'public, max-age=600'); // 10 min cache
res.json(result);
} catch (error) {
console.error('Error fetching data:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
router.get('/portnum-counts', async (req, res) => {
const nodeId = req.query.nodeId ? parseInt(req.query.nodeId, 10) : null;
const hours = 24;
const now = new Date();
const startTime = new Date(now.getTime() - hours * 60 * 60 * 1000);
try {
const envelopes = await prisma.serviceEnvelope.findMany({
where: {
created_at: { gte: startTime },
...(Number.isInteger(nodeId) ? { from: nodeId } : {}),
packet_id: { not: null },
to: { not: 1 }, // Filter out NODENUM_BROADCAST_NO_LORA
OR: [
{ portnum: { not: 73 } }, // Exclude portnum 73 (e.g. map reports)
{ portnum: null } // But include PKI packages, they have no portnum
]
},
select: {from: true, packet_id: true, portnum: true, channel_id: true}
});
// Ensure uniqueness based on (from, packet_id)
const seen = new Set();
const counts = {};
for (const envelope of envelopes) {
const uniqueKey = `${envelope.from}-${envelope.packet_id}`;
if (seen.has(uniqueKey)) continue;
seen.add(uniqueKey);
// Override portnum to 512 if channel_id is "PKI"
const portnum = envelope.channel_id === "PKI" ? 512 : (envelope.portnum ?? 0);
counts[portnum] = (counts[portnum] || 0) + 1;
}
const result = Object.entries(counts).map(([portnum, count]) => ({
portnum: parseInt(portnum, 10),
count: count,
label: parseInt(portnum, 10) === 512 ? "PKI" : (PortNum.valuesById[portnum] ?? "UNKNOWN"),
})).sort((a, b) => a.portnum - b.portnum);
res.json(result);
} catch (err) {
console.error("Error in /portnum-counts:", err);
res.status(500).json({ message: "Internal server error" });
}
});
router.get('/battery-stats', async (req, res) => {
const days = parseInt(req.query.days || '1', 10);
try {
const stats = await prisma.$queryRaw`
SELECT id, recorded_at, avg_battery_level
FROM battery_stats
WHERE recorded_at >= NOW() - INTERVAL ${days} DAY
ORDER BY recorded_at DESC;
`;
res.json(stats);
} catch (err) {
console.error('Error fetching battery stats:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
router.get('/channel-utilization-stats', async (req, res) => {
const days = parseInt(req.query.days || '1', 10);
try {
const stats = await prisma.$queryRaw`
SELECT recorded_at, avg_channel_utilization
FROM channel_utilization_stats
WHERE recorded_at >= NOW() - INTERVAL ${days} DAY
ORDER BY recorded_at DESC;
`;
res.json(stats);
} catch (err) {
console.error('Error fetching channel utilization stats:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;