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
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
# Keep environment variables out of version control
.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
# add project files to /app
ADD ./ /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
# 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
> 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.
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": "",
"license": "ISC",
"dependencies": {
"@prisma/client": "^6.13.0",
"command-line-args": "^6.0.1",
"command-line-usage": "^7.0.3",
"compression": "^1.8.1",
"cors": "^2.8.5",
"@prisma/client": "^5.11.0",
"command-line-args": "^5.2.1",
"command-line-usage": "^7.0.1",
"compression": "^1.7.4",
"express": "^5.0.0",
"mqtt": "^5.14.0",
"protobufjs": "^7.5.3"
"mqtt": "^5.3.6",
"protobufjs": "^7.2.6"
},
"devDependencies": {
"jest": "^30.0.5",
"prisma": "^6.13.0"
"jest": "^29.7.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
role Int
is_licensed Boolean?
public_key String?
is_unmessagable Boolean?
firmware_version String?
region Int?
@@ -53,9 +51,6 @@ model Node {
// this column tracks when an mqtt gateway node uplinked a packet
mqtt_connection_state_updated_at DateTime?
ok_to_mqtt Boolean?
is_backbone Boolean?
created_at DateTime @default(now())
updated_at DateTime @default(now()) @updatedAt
@@ -207,8 +202,6 @@ model ServiceEnvelope {
gateway_id BigInt?
to BigInt
from BigInt
portnum Int?
packet_id BigInt?
protobuf Bytes
created_at DateTime @default(now())
@@ -217,7 +210,6 @@ model ServiceEnvelope {
@@index(created_at)
@@index(updated_at)
@@index(gateway_id)
@@index(packet_id)
@@map("service_envelopes")
}
@@ -304,41 +296,3 @@ model Waypoint {
@@index(gateway_id)
@@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 express = require('express');
const compression = require('compression');
const protobufjs = require("protobufjs");
const commandLineArgs = require("command-line-args");
const commandLineUsage = require("command-line-usage");
const cors = require('cors');
const statsRoutes = require('./stats.js');
// create prisma db client
const { PrismaClient } = require("@prisma/client");
@@ -53,24 +50,21 @@ if(options.help){
// get options and fallback to default values
const port = options["port"] ?? 8080;
// 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 Role = root.lookupEnum("Config.DeviceConfig.Role");
const RegionCode = root.lookupEnum("Config.LoRaConfig.RegionCode");
const ModemPreset = root.lookupEnum("Config.LoRaConfig.ModemPreset");
// load json
const hardwareModels = JSON.parse(fs.readFileSync(path.join(__dirname, "json/hardware_models.json"), "utf-8"));
const roles = JSON.parse(fs.readFileSync(path.join(__dirname, "json/roles.json"), "utf-8"));
const regionCodes = JSON.parse(fs.readFileSync(path.join(__dirname, "json/region_codes.json"), "utf-8"));
const modemPresets = JSON.parse(fs.readFileSync(path.join(__dirname, "json/modem_presets.json"), "utf-8"));
// appends extra info for node objects returned from api
function formatNodeInfo(node) {
return {
...node,
node_id_hex: "!" + node.node_id.toString(16),
hardware_model_name: HardwareModel.valuesById[node.hardware_model] ?? null,
role_name: Role.valuesById[node.role] ?? null,
region_name: RegionCode.valuesById[node.region] ?? null,
modem_preset_name: ModemPreset.valuesById[node.modem_preset] ?? null,
hardware_model_name: hardwareModels[node.hardware_model] ?? null,
role_name: roles[node.role] ?? null,
region_name: regionCodes[node.region] ?? null,
modem_preset_name: modemPresets[node.modem_preset] ?? null,
};
}
@@ -79,9 +73,6 @@ const app = express();
// enable compression
app.use(compression());
// Apply CORS only to API routes
app.use('/api', cors());
// serve files inside the public folder from /
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'));
});
// stats API in separate file
app.use('/api/v1/stats', statsRoutes);
app.get('/api', async (req, res) => {
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) => {
try {
@@ -781,7 +805,6 @@ app.get('/api/v1/waypoints', async (req, res) => {
}
});
// start express server
const listener = app.listen(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 path = require("path");
const mqtt = require("mqtt");
@@ -21,6 +22,11 @@ const optionsList = [
type: Boolean,
description: 'Display this usage guide.'
},
{
name: "protobufs-path",
type: String,
description: "Path to Protobufs (e.g: ../../protobufs)",
},
{
name: "mqtt-broker-url",
type: String,
@@ -206,6 +212,7 @@ if(options.help){
}
// 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 mqttUsername = options["mqtt-username"] ?? "meshdev";
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 decryptionKeys = options["decryption-keys"] ?? [
"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 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 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
const client = mqtt.connect(mqttBrokerUrl, {
username: mqttUsername,
@@ -250,7 +275,7 @@ const client = mqtt.connect(mqttBrokerUrl, {
// load protobufs
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');
const Data = root.lookupType("Data");
const ServiceEnvelope = root.lookupType("ServiceEnvelope");
@@ -751,8 +776,6 @@ client.on("message", async (topic, message) => {
gateway_id: envelope.gatewayId ? convertHexIdToNumericId(envelope.gatewayId) : null,
to: envelope.packet.to,
from: envelope.packet.from,
portnum: portnum,
packet_id: envelope.packet.id,
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
try {
await prisma.node.upsert({
@@ -945,17 +961,6 @@ client.on("message", async (topic, message) => {
hardware_model: user.hwModel,
is_licensed: user.isLicensed === true,
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: {
long_name: user.longName,
@@ -963,45 +968,12 @@ client.on("message", async (topic, message) => {
hardware_model: user.hwModel,
is_licensed: user.isLicensed === true,
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) {
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) {
@@ -1439,6 +1411,6 @@ client.on("message", async (topic, message) => {
}
} 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 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="description" content="An interactive map of all Meshtastic nodes.">
<link rel="icon" type="image/png" href="/icon.png"/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://map.sthlm-mesh.se">
<meta property="og:title" content="STHLM-MESH MAP">
<meta property="og:url" content="https://meshtastic.liamcottle.net">
<meta property="og:title" content="Meshtastic Map">
<meta property="og:description" content="An interactive map of all Meshtastic nodes.">
<!-- tailwind css -->
@@ -56,7 +56,7 @@
}
.icon-mqtt-connected {
background-color: #2563eb; /* Change to use same color as disconnected // #16a34a; */
background-color: #16a34a;
border-radius: 25px;
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">
<!-- 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 -->
<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 -->
<div v-if="isShowingMobileSearch" class="my-auto">
@@ -160,13 +184,16 @@
</div>
<!-- icon -->
<div v-if="!isShowingMobileSearch" class="hidden sm:block my-auto mr-2 ml-2">
<img class="w-8 h-8 rounded" src="icon.png"/>
<div v-if="!isShowingMobileSearch" class="hidden sm:block my-auto mr-3">
<img class="w-10 h-10 rounded" src="icon.png"/>
</div>
<!-- app info -->
<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>
<!-- search bar -->
@@ -226,6 +253,30 @@
<span class="tooltip-text">Search</span>
</div>
</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">
<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">
@@ -309,35 +360,112 @@
<img src="./icon.png" class="mx-auto w-16 h-16 rounded mb-1"/>
<h1 class="font-bold">Meshtastic Map</h1>
<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>
<!-- 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 class="font-bold mb-2">Beskrivning</div>
<div class="space-y-2">
<div class="bg-gray-100 rounded p-2 border border-gray-200">
<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>
</div>
<div class="font-bold mb-2">Features</div>
<div class="bg-gray-100 rounded p-2 border border-gray-200">
<ul class="list-disc list-inside">
<li>The map shows nodes that have sent a valid position to MQTT.</li>
<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 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="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>Då vi enbart vill analysera Meshen i stockholm är MQTT servern inte öppen för alla.</div>
<div>Vill du koppla upp din nod, kontakta @Roslund på Discord.</div>
<div class="font-semibold">How do I add my node to the map?</div>
<div>Your node, or a node that hears your node must uplink to our MQTT server.</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 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">
<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>JSON Output: No</li>
<li>TLS Enabled: No</li>
</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>
<!-- legal -->
<div>
<div class="font-bold mb-2">Legal</div>
@@ -600,6 +728,41 @@
</ul>
</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 -->
<div>
<div @click.stop class="flex bg-gray-200 p-2 font-semibold">
@@ -642,7 +805,6 @@
<option value="1d">1 Day</option>
<option value="3d">3 Days</option>
<option value="7d">7 Days</option>
<option value="30d">30 Days</option>
</select>
</div>
</div>
@@ -733,7 +895,6 @@
<option value="1d">1 Day</option>
<option value="3d">3 Days</option>
<option value="7d">7 Days</option>
<option value="30d">30 Days</option>
</select>
</div>
</div>
@@ -758,10 +919,6 @@
<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>
<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>
@@ -807,7 +964,6 @@
<option value="1d">1 Day</option>
<option value="3d">3 Days</option>
<option value="7d">7 Days</option>
<option value="30d">30 Days</option>
</select>
</div>
</div>
@@ -1672,9 +1828,9 @@
selectedNodeMqttMetrics: [],
selectedNodeTraceroutes: [],
deviceMetricsTimeRange: "7d",
environmentMetricsTimeRange: "7d",
powerMetricsTimeRange: "7d",
deviceMetricsTimeRange: "3d",
environmentMetricsTimeRange: "3d",
powerMetricsTimeRange: "3d",
isPositionHistoryModalExpanded: true,
positionHistoryDateTimeFrom: null,
@@ -1736,7 +1892,7 @@
methods: {
getAnnouncementId: function() {
// change this when making a new announcement
return "1";
return "2";
},
shouldShowAnnouncement: function() {
const lastSeenAnnouncementId = window.localStorage.getItem("last-seen-announcement-id");
@@ -1763,7 +1919,6 @@
const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 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
var timeFrom = threeDaysAgoInMilliseconds;
@@ -1780,10 +1935,6 @@
timeFrom = sevenDaysAgoInMilliseconds;
break;
}
case "30d": {
timeFrom = thirtyDaysAgoInMilliseconds;
break;
}
}
window.axios.get(`/api/v1/nodes/${nodeId}/device-metrics`, {
@@ -1805,7 +1956,6 @@
const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 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
var timeFrom = threeDaysAgoInMilliseconds;
@@ -1822,10 +1972,6 @@
timeFrom = sevenDaysAgoInMilliseconds;
break;
}
case "30d": {
timeFrom = thirtyDaysAgoInMilliseconds;
break;
}
}
window.axios.get(`/api/v1/nodes/${nodeId}/environment-metrics`, {
@@ -1847,7 +1993,6 @@
const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000);
const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 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
var timeFrom = threeDaysAgoInMilliseconds;
@@ -1864,10 +2009,6 @@
timeFrom = sevenDaysAgoInMilliseconds;
break;
}
case "30d": {
timeFrom = thirtyDaysAgoInMilliseconds;
break;
}
}
window.axios.get(`/api/v1/nodes/${nodeId}/power-metrics`, {
@@ -2061,13 +2202,11 @@
const temperatureMetrics = [];
const relativeHumidityMetrics = [];
const barometricPressureMetrics = [];
const iaqMetrics = [];
for(const deviceMetric of this.selectedNodeEnvironmentMetrics){
labels.push(moment(deviceMetric.created_at));
temperatureMetrics.push(deviceMetric.temperature);
relativeHumidityMetrics.push(deviceMetric.relative_humidity);
barometricPressureMetrics.push(deviceMetric.barometric_pressure);
iaqMetrics.push(deviceMetric.iaq);
}
// create chart
@@ -2107,17 +2246,6 @@
yAxisID: 'y1',
},
{
label: 'IAQ',
suffix: 'IAQ',
borderColor: '#f472b6',
backgroundColor: '#f472b6',
pointStyle: false, // no points
fill: false,
data: iaqMetrics,
yAxisID: 'yIAQ',
},
],
},
options: {
@@ -2156,10 +2284,6 @@
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
},
yIAQ: {
type: 'linear',
display: false,
},
},
plugins: {
legend: {
@@ -2602,13 +2726,13 @@
[100, 500], // bottom right
];
// create map positioned over Stockholm
// create map positioned over AU and NZ
var map = L.map('map', {
maxBounds: bounds,
}).setView([
59.3,
378.1,
], 10);
-15,
150,
], 2);
// remove leaflet link
map.attributionControl.setPrefix('');
@@ -2656,7 +2780,6 @@
// create layer groups
var nodesLayerGroup = new L.LayerGroup();
var neighboursLayerGroup = new L.LayerGroup();
var backboneNeighboursLayerGroup = new L.LayerGroup();
var nodeNeighboursLayerGroup = new L.LayerGroup();
var nodesClusteredLayerGroup = L.markerClusterGroup({
showCoverageOnHover: false,
@@ -2666,7 +2789,6 @@
showCoverageOnHover: false,
disableClusteringAtZoom: 10, // zoom level where node clustering is disabled
});
var nodesBackboneLayerGroup = new L.LayerGroup();
var waypointsLayerGroup = new L.LayerGroup();
var nodePositionHistoryLayerGroup = new L.LayerGroup();
@@ -2726,14 +2848,12 @@
"Nodes": {
"All": nodesLayerGroup,
"Routers": nodesRouterLayerGroup,
"Backbone": nodesBackboneLayerGroup,
"Clustered": nodesClusteredLayerGroup,
"None": new L.LayerGroup(),
},
"Overlays": {
"Legend": legendLayerGroup,
"Neighbours": neighboursLayerGroup,
"Backbone Connection": backboneNeighboursLayerGroup,
"Waypoints": waypointsLayerGroup,
"Position History": nodePositionHistoryLayerGroup,
},
@@ -2743,7 +2863,7 @@
}).addTo(map);
// enable base layers
nodesLayerGroup.addTo(map);
nodesClusteredLayerGroup.addTo(map);
// enable overlay layers based on config
const enabledOverlayLayers = getConfigMapEnabledOverlayLayers();
@@ -2753,9 +2873,6 @@
if(enabledOverlayLayers.includes("Neighbours")){
neighboursLayerGroup.addTo(map);
}
if(enabledOverlayLayers.includes("Backbone Connection")){
backboneNeighboursLayerGroup.addTo(map);
}
if(enabledOverlayLayers.includes("Waypoints")){
waypointsLayerGroup.addTo(map);
}
@@ -2901,12 +3018,10 @@
nodesLayerGroup.clearLayers();
nodesClusteredLayerGroup.clearLayers();
nodesRouterLayerGroup.clearLayers();
nodesBackboneLayerGroup.clearLayers();
}
function clearAllNeighbours() {
neighboursLayerGroup.clearLayers();
backboneNeighboursLayerGroup.clearLayers();
}
function clearAllWaypoints() {
@@ -2980,9 +3095,8 @@
}
function getColourForSnr(snr) {
if(snr >= -5) return "#16a34a"; // good
if(snr > -15) return "#fff200"; // meh
if(snr <= -15) return "#dc2626"; // bad
if(snr >= 0) return "#16a34a"; // good
if(snr < 0) return "#dc2626"; // bad
}
function cleanUpNodeNeighbours() {
@@ -3007,13 +3121,13 @@
const node1MarkerColour = "0000FF"; // blue
const node1Latitude = node1.latitude;
const node1Longitude = node1.longitude;
const node1ElevationMSL = node1.altitude ?? "";
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 ?? "";
const node2ElevationMSL = ""; // node2.altitude ?? "";
// generate terrain profile image url
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>`
+ `<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>`;
@@ -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>`
+ `<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()}` : '')
+ `<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>`;
@@ -3354,15 +3472,9 @@
if(nodeHasUplinkedToMqttRecently){
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
const marker = L.marker([node.latitude + jitter, longitude + jitter], {
const marker = L.marker([node.latitude, longitude], {
icon: icon,
tagName: node.node_id,
// we want to show online nodes above offline, but without needing to use separate layer groups
@@ -3384,11 +3496,6 @@
nodesRouterLayerGroup.addLayer(marker);
}
// add markers for backbone to layer group
if(node.is_backbone) {
nodesBackboneLayerGroup.addLayer(marker);
}
// show tooltip on desktop only
if(!isMobile()){
marker.bindTooltip(getTooltipContentForNode(node), {
@@ -3434,6 +3541,7 @@
}
// add node neighbours
var polylineOffset = 0;
const neighbours = node.neighbours ?? [];
for(const neighbour of neighbours){
@@ -3458,10 +3566,6 @@
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
const line = L.polyline([
currentNode.getLatLng(),
@@ -3469,31 +3573,11 @@
], {
color: '#2563eb',
opacity: 0.75,
// if we have a symmetrical connection, offset the the line so they don't overlapp
offset: symmetrical ? 3 : 0,
offset: polylineOffset,
}).addTo(neighboursLayerGroup);
// additional line for backbone neighbours
const backboneNeighbourLine = L.polyline([
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);
}
// increase offset so next neighbour does not overlay other neighbours from self
polylineOffset += 2;
// default to showing distance in 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>`
+ `<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>`;
@@ -3524,17 +3610,6 @@
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/>Hardware: ${node.hardware_model_name}` +
(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 > 100){
@@ -3983,15 +4060,9 @@
</script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2RD5193D15"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-2RD5193D15');
</script>
<!-- analytics -->
<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><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>
</body>
</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;