forked from iarv/meshtastic-map
Compare commits
103 Commits
docker-imp
...
additional
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ee526caf7 | ||
|
|
a7b99c3027 | ||
|
|
b668892248 | ||
|
|
0053b9f774 | ||
|
|
435c122c21 | ||
|
|
c5028f911f | ||
|
|
7fee84de77 | ||
|
|
162f8da79c | ||
|
|
ca9d1d9de0 | ||
|
|
8eb5c695f0 | ||
|
|
e11367544d | ||
|
|
cbcbeb9a22 | ||
|
|
4c745123c1 | ||
|
|
800dcfef78 | ||
|
|
e27f92d5c6 | ||
|
|
772faa550b | ||
|
|
a716d401a9 | ||
|
|
aeeb95554b | ||
|
|
bc78a0b0e3 | ||
|
|
aa57eb543f | ||
|
|
d5993aebb8 | ||
|
|
2b2ca982a0 | ||
|
|
d685826f15 | ||
|
|
91f98a6a09 | ||
|
|
3452efb1b7 | ||
|
|
54e83e5cdd | ||
|
|
d3ac48f044 | ||
|
|
24b90f6710 | ||
|
|
a77518facf | ||
|
|
c36a797967 | ||
|
|
5c83393cb1 | ||
|
|
5c0a7db8c6 | ||
|
|
201ba0d399 | ||
|
|
b31d10d434 | ||
|
|
96cc1ad1d5 | ||
|
|
ce2121d964 | ||
|
|
0c5b330681 | ||
|
|
2d1dd6fbb2 | ||
|
|
9e17347a7a | ||
|
|
6619e42bad | ||
|
|
e95bac1063 | ||
|
|
3eed255eb4 | ||
|
|
2bc82ae7a3 | ||
|
|
9d7a60efa5 | ||
|
|
8c064ac6e6 | ||
|
|
b3bb02fede | ||
|
|
11f5898996 | ||
|
|
17be3cb6a9 | ||
|
|
d95af37be5 | ||
|
|
e30fb12aa8 | ||
|
|
6691df73f5 | ||
|
|
9b69d0ce27 | ||
|
|
cf3f053e12 | ||
|
|
305f091142 | ||
|
|
4d1bdba6e0 | ||
|
|
6210f04ea5 | ||
|
|
99e31d8692 | ||
|
|
55cbdb63ba | ||
|
|
c251c5adb8 | ||
|
|
189c89a7ca | ||
|
|
8b84a3a91c | ||
|
|
6cd07fe314 | ||
|
|
fac5a91829 | ||
|
|
2fbaab81c5 | ||
|
|
bfb845ac37 | ||
|
|
274f0b8efa | ||
|
|
c8f322f012 | ||
|
|
f35a8876f9 | ||
|
|
af2a663dab | ||
|
|
342c8dc87a | ||
|
|
ff8bb07f7f | ||
|
|
9411c9e4cc | ||
|
|
7ab62a9968 | ||
|
|
a3c4667f34 | ||
|
|
700ee298c4 | ||
|
|
4c6bccc058 | ||
|
|
cdcefee641 | ||
|
|
6e9d425bda | ||
|
|
984771925f | ||
|
|
cadc78d1d4 | ||
|
|
fd36e1b0a2 | ||
|
|
c3c92b47f1 | ||
|
|
48e25dd352 | ||
|
|
708451b027 | ||
|
|
37e54c76a6 | ||
|
|
b319115fd2 | ||
|
|
1eb0b7eeea | ||
|
|
67fc07d326 | ||
|
|
24df50889d | ||
|
|
1f93c8eb9e | ||
|
|
566b8d6086 | ||
|
|
2b6156ff07 | ||
|
|
3c12e5fbe9 | ||
|
|
ec8093a1ca | ||
|
|
55c7a45060 | ||
|
|
471d8dded6 | ||
|
|
045008a9df | ||
|
|
fd50a4adeb | ||
|
|
efdf5f850d | ||
|
|
15bd3ebdc8 | ||
|
|
a73d858ebd | ||
|
|
af92253092 | ||
|
|
7e5a026987 |
@@ -1,3 +1,2 @@
|
||||
.env
|
||||
node_modules
|
||||
.git
|
||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# 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
2
.gitignore
vendored
@@ -2,5 +2,3 @@
|
||||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
|
||||
src/external
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "protobufs"]
|
||||
path = src/protobufs
|
||||
url = https://github.com/meshtastic/protobufs.git
|
||||
14
Dockerfile
14
Dockerfile
@@ -1,16 +1,12 @@
|
||||
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
|
||||
|
||||
@@ -122,9 +122,6 @@ 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.
|
||||
|
||||
|
||||
4199
package-lock.json
generated
4199
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -9,16 +9,17 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.11.0",
|
||||
"command-line-args": "^5.2.1",
|
||||
"command-line-usage": "^7.0.1",
|
||||
"compression": "^1.7.4",
|
||||
"@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",
|
||||
"express": "^5.0.0",
|
||||
"mqtt": "^5.3.6",
|
||||
"protobufjs": "^7.2.6"
|
||||
"mqtt": "^5.14.0",
|
||||
"protobufjs": "^7.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0",
|
||||
"prisma": "^5.10.2"
|
||||
"jest": "^30.0.5",
|
||||
"prisma": "^6.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `nodes` ADD COLUMN `ok_to_mqtt` BOOLEAN NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `service_envelopes` ADD COLUMN `portnum` INTEGER NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `service_envelopes` ADD COLUMN `packet_id` BIGINT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX `service_envelopes_packet_id_idx` ON `service_envelopes`(`packet_id`);
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `nodes` ADD COLUMN `is_backbone` BOOLEAN NULL;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `nodes` ADD COLUMN `is_unmessagable` BOOLEAN NULL,
|
||||
ADD COLUMN `public_key` VARCHAR(191) NULL;
|
||||
@@ -21,6 +21,8 @@ model Node {
|
||||
hardware_model Int
|
||||
role Int
|
||||
is_licensed Boolean?
|
||||
public_key String?
|
||||
is_unmessagable Boolean?
|
||||
|
||||
firmware_version String?
|
||||
region Int?
|
||||
@@ -51,6 +53,9 @@ 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
|
||||
|
||||
@@ -202,6 +207,8 @@ model ServiceEnvelope {
|
||||
gateway_id BigInt?
|
||||
to BigInt
|
||||
from BigInt
|
||||
portnum Int?
|
||||
packet_id BigInt?
|
||||
protobuf Bytes
|
||||
|
||||
created_at DateTime @default(now())
|
||||
@@ -210,6 +217,7 @@ model ServiceEnvelope {
|
||||
@@index(created_at)
|
||||
@@index(updated_at)
|
||||
@@index(gateway_id)
|
||||
@@index(packet_id)
|
||||
@@map("service_envelopes")
|
||||
}
|
||||
|
||||
@@ -296,3 +304,41 @@ 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")
|
||||
}
|
||||
69
src/index.js
69
src/index.js
@@ -1,9 +1,12 @@
|
||||
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");
|
||||
@@ -50,21 +53,24 @@ if(options.help){
|
||||
// get options and fallback to default values
|
||||
const port = options["port"] ?? 8080;
|
||||
|
||||
// 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"));
|
||||
// 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");
|
||||
|
||||
// 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: 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,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,6 +79,9 @@ 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')));
|
||||
|
||||
@@ -80,6 +89,9 @@ 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 = [
|
||||
@@ -643,42 +655,6 @@ 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 {
|
||||
|
||||
@@ -805,6 +781,7 @@ app.get('/api/v1/waypoints', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// start express server
|
||||
const listener = app.listen(port, () => {
|
||||
const port = listener.address().port;
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
84
src/mqtt.js
84
src/mqtt.js
@@ -1,4 +1,3 @@
|
||||
const fs = require("fs");
|
||||
const crypto = require("crypto");
|
||||
const path = require("path");
|
||||
const mqtt = require("mqtt");
|
||||
@@ -22,11 +21,6 @@ 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,
|
||||
@@ -212,7 +206,6 @@ 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";
|
||||
@@ -229,6 +222,7 @@ 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;
|
||||
@@ -247,25 +241,6 @@ 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,
|
||||
@@ -275,7 +250,7 @@ const client = mqtt.connect(mqttBrokerUrl, {
|
||||
|
||||
// load protobufs
|
||||
const root = new protobufjs.Root();
|
||||
root.resolvePath = (origin, target) => path.join(protobufsPath, target);
|
||||
root.resolvePath = (origin, target) => path.join(__dirname, "protobufs", target);
|
||||
root.loadSync('meshtastic/mqtt.proto');
|
||||
const Data = root.lookupType("Data");
|
||||
const ServiceEnvelope = root.lookupType("ServiceEnvelope");
|
||||
@@ -776,6 +751,8 @@ 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,
|
||||
},
|
||||
});
|
||||
@@ -948,6 +925,13 @@ 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({
|
||||
@@ -961,6 +945,17 @@ 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,
|
||||
@@ -968,12 +963,45 @@ 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) {
|
||||
@@ -1411,6 +1439,6 @@ client.on("message", async (topic, message) => {
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// ignore errors
|
||||
console.log("error", e);
|
||||
}
|
||||
});
|
||||
|
||||
1
src/protobufs
Submodule
1
src/protobufs
Submodule
Submodule src/protobufs added at 1ecf94da98
BIN
src/public/images/devices/HELTEC_VISION_MASTER_E213.png
Normal file
BIN
src/public/images/devices/HELTEC_VISION_MASTER_E213.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -3,15 +3,15 @@
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>Meshtastic Map</title>
|
||||
<title>STHLM-MESH 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://meshtastic.liamcottle.net">
|
||||
<meta property="og:title" content="Meshtastic Map">
|
||||
<meta property="og:url" content="https://map.sthlm-mesh.se">
|
||||
<meta property="og:title" content="STHLM-MESH MAP">
|
||||
<meta property="og:description" content="An interactive map of all Meshtastic nodes.">
|
||||
|
||||
<!-- tailwind css -->
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
|
||||
.icon-mqtt-connected {
|
||||
background-color: #16a34a;
|
||||
background-color: #2563eb; /* Change to use same color as disconnected // #16a34a; */
|
||||
border-radius: 25px;
|
||||
border: 1px solid white;
|
||||
}
|
||||
@@ -145,32 +145,8 @@
|
||||
<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 bg-white p-2 border-gray-300 border-b h-16">
|
||||
<div class="flex p-3 h-16" style="background-color: 30a552;">
|
||||
|
||||
<!-- close mobile search button -->
|
||||
<div v-if="isShowingMobileSearch" class="my-auto">
|
||||
@@ -184,16 +160,13 @@
|
||||
</div>
|
||||
|
||||
<!-- icon -->
|
||||
<div v-if="!isShowingMobileSearch" class="hidden sm:block my-auto mr-3">
|
||||
<img class="w-10 h-10 rounded" src="icon.png"/>
|
||||
<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>
|
||||
|
||||
<!-- app info -->
|
||||
<div v-if="!isShowingMobileSearch" class="my-auto leading-tight">
|
||||
<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>
|
||||
<a href="https://sthlm-mesh.se"><div class="font-bold" style="color: #ffffff; font-size: 1.25rem;">STHLM-MESH</div></a>
|
||||
</div>
|
||||
|
||||
<!-- search bar -->
|
||||
@@ -253,30 +226,6 @@
|
||||
<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">
|
||||
@@ -360,112 +309,35 @@
|
||||
<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>
|
||||
<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>
|
||||
<h2>Forked by <a class="link" target="_blank" href="http://github.com/Roslund/">Roslund</a></h2>
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
<!-- Beskrivning -->
|
||||
<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>
|
||||
|
||||
<!-- faq -->
|
||||
<div>
|
||||
<div class="font-bold mb-2">FAQ</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 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>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>
|
||||
<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 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>
|
||||
<div class="bg-gray-100 rounded p-2 border border-gray-200">
|
||||
<div class="font-semibold">What MQTT server should I use?</div>
|
||||
<div class="font-semibold">Inställningar:</div>
|
||||
<ul class="list-disc list-inside">
|
||||
<li>Address: mqtt.meshtastic.liamcottle.net</li>
|
||||
<li>Username: uplink</li>
|
||||
<li>Password: uplink</li>
|
||||
<li>Address: mqtt.sthlm-mesh.se</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>
|
||||
@@ -728,41 +600,6 @@
|
||||
</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">
|
||||
@@ -805,6 +642,7 @@
|
||||
<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>
|
||||
@@ -895,6 +733,7 @@
|
||||
<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>
|
||||
@@ -919,6 +758,10 @@
|
||||
<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>
|
||||
@@ -964,6 +807,7 @@
|
||||
<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>
|
||||
@@ -1828,9 +1672,9 @@
|
||||
selectedNodeMqttMetrics: [],
|
||||
selectedNodeTraceroutes: [],
|
||||
|
||||
deviceMetricsTimeRange: "3d",
|
||||
environmentMetricsTimeRange: "3d",
|
||||
powerMetricsTimeRange: "3d",
|
||||
deviceMetricsTimeRange: "7d",
|
||||
environmentMetricsTimeRange: "7d",
|
||||
powerMetricsTimeRange: "7d",
|
||||
|
||||
isPositionHistoryModalExpanded: true,
|
||||
positionHistoryDateTimeFrom: null,
|
||||
@@ -1892,7 +1736,7 @@
|
||||
methods: {
|
||||
getAnnouncementId: function() {
|
||||
// change this when making a new announcement
|
||||
return "2";
|
||||
return "1";
|
||||
},
|
||||
shouldShowAnnouncement: function() {
|
||||
const lastSeenAnnouncementId = window.localStorage.getItem("last-seen-announcement-id");
|
||||
@@ -1919,6 +1763,7 @@
|
||||
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;
|
||||
@@ -1935,6 +1780,10 @@
|
||||
timeFrom = sevenDaysAgoInMilliseconds;
|
||||
break;
|
||||
}
|
||||
case "30d": {
|
||||
timeFrom = thirtyDaysAgoInMilliseconds;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.axios.get(`/api/v1/nodes/${nodeId}/device-metrics`, {
|
||||
@@ -1956,6 +1805,7 @@
|
||||
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;
|
||||
@@ -1972,6 +1822,10 @@
|
||||
timeFrom = sevenDaysAgoInMilliseconds;
|
||||
break;
|
||||
}
|
||||
case "30d": {
|
||||
timeFrom = thirtyDaysAgoInMilliseconds;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.axios.get(`/api/v1/nodes/${nodeId}/environment-metrics`, {
|
||||
@@ -1993,6 +1847,7 @@
|
||||
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;
|
||||
@@ -2009,6 +1864,10 @@
|
||||
timeFrom = sevenDaysAgoInMilliseconds;
|
||||
break;
|
||||
}
|
||||
case "30d": {
|
||||
timeFrom = thirtyDaysAgoInMilliseconds;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.axios.get(`/api/v1/nodes/${nodeId}/power-metrics`, {
|
||||
@@ -2202,11 +2061,13 @@
|
||||
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
|
||||
@@ -2246,6 +2107,17 @@
|
||||
yAxisID: 'y1',
|
||||
|
||||
},
|
||||
{
|
||||
label: 'IAQ',
|
||||
suffix: 'IAQ',
|
||||
borderColor: '#f472b6',
|
||||
backgroundColor: '#f472b6',
|
||||
pointStyle: false, // no points
|
||||
fill: false,
|
||||
data: iaqMetrics,
|
||||
yAxisID: 'yIAQ',
|
||||
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
@@ -2284,6 +2156,10 @@
|
||||
drawOnChartArea: false, // only want the grid lines for one axis to show up
|
||||
},
|
||||
},
|
||||
yIAQ: {
|
||||
type: 'linear',
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
@@ -2726,13 +2602,13 @@
|
||||
[100, 500], // bottom right
|
||||
];
|
||||
|
||||
// create map positioned over AU and NZ
|
||||
// create map positioned over Stockholm
|
||||
var map = L.map('map', {
|
||||
maxBounds: bounds,
|
||||
}).setView([
|
||||
-15,
|
||||
150,
|
||||
], 2);
|
||||
59.3,
|
||||
378.1,
|
||||
], 10);
|
||||
|
||||
// remove leaflet link
|
||||
map.attributionControl.setPrefix('');
|
||||
@@ -2780,6 +2656,7 @@
|
||||
// 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,
|
||||
@@ -2789,6 +2666,7 @@
|
||||
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();
|
||||
|
||||
@@ -2848,12 +2726,14 @@
|
||||
"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,
|
||||
},
|
||||
@@ -2863,7 +2743,7 @@
|
||||
}).addTo(map);
|
||||
|
||||
// enable base layers
|
||||
nodesClusteredLayerGroup.addTo(map);
|
||||
nodesLayerGroup.addTo(map);
|
||||
|
||||
// enable overlay layers based on config
|
||||
const enabledOverlayLayers = getConfigMapEnabledOverlayLayers();
|
||||
@@ -2873,6 +2753,9 @@
|
||||
if(enabledOverlayLayers.includes("Neighbours")){
|
||||
neighboursLayerGroup.addTo(map);
|
||||
}
|
||||
if(enabledOverlayLayers.includes("Backbone Connection")){
|
||||
backboneNeighboursLayerGroup.addTo(map);
|
||||
}
|
||||
if(enabledOverlayLayers.includes("Waypoints")){
|
||||
waypointsLayerGroup.addTo(map);
|
||||
}
|
||||
@@ -3018,10 +2901,12 @@
|
||||
nodesLayerGroup.clearLayers();
|
||||
nodesClusteredLayerGroup.clearLayers();
|
||||
nodesRouterLayerGroup.clearLayers();
|
||||
nodesBackboneLayerGroup.clearLayers();
|
||||
}
|
||||
|
||||
function clearAllNeighbours() {
|
||||
neighboursLayerGroup.clearLayers();
|
||||
backboneNeighboursLayerGroup.clearLayers();
|
||||
}
|
||||
|
||||
function clearAllWaypoints() {
|
||||
@@ -3095,8 +2980,9 @@
|
||||
}
|
||||
|
||||
function getColourForSnr(snr) {
|
||||
if(snr >= 0) return "#16a34a"; // good
|
||||
if(snr < 0) return "#dc2626"; // bad
|
||||
if(snr >= -5) return "#16a34a"; // good
|
||||
if(snr > -15) return "#fff200"; // meh
|
||||
if(snr <= -15) return "#dc2626"; // bad
|
||||
}
|
||||
|
||||
function cleanUpNodeNeighbours() {
|
||||
@@ -3121,13 +3007,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({
|
||||
@@ -3227,8 +3113,6 @@
|
||||
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>`;
|
||||
@@ -3348,8 +3232,6 @@
|
||||
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>`;
|
||||
@@ -3472,9 +3354,15 @@
|
||||
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, longitude], {
|
||||
const marker = L.marker([node.latitude + jitter, longitude + jitter], {
|
||||
icon: icon,
|
||||
tagName: node.node_id,
|
||||
// we want to show online nodes above offline, but without needing to use separate layer groups
|
||||
@@ -3496,6 +3384,11 @@
|
||||
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), {
|
||||
@@ -3541,7 +3434,6 @@
|
||||
}
|
||||
|
||||
// add node neighbours
|
||||
var polylineOffset = 0;
|
||||
const neighbours = node.neighbours ?? [];
|
||||
for(const neighbour of neighbours){
|
||||
|
||||
@@ -3566,6 +3458,10 @@
|
||||
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(),
|
||||
@@ -3573,11 +3469,31 @@
|
||||
], {
|
||||
color: '#2563eb',
|
||||
opacity: 0.75,
|
||||
offset: polylineOffset,
|
||||
// if we have a symmetrical connection, offset the the line so they don't overlapp
|
||||
offset: symmetrical ? 3 : 0,
|
||||
}).addTo(neighboursLayerGroup);
|
||||
|
||||
// increase offset so next neighbour does not overlay other neighbours from self
|
||||
polylineOffset += 2;
|
||||
// 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);
|
||||
}
|
||||
|
||||
// default to showing distance in meters
|
||||
var distance = `${distanceInMeters} meters`;
|
||||
@@ -3593,8 +3509,6 @@
|
||||
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>`;
|
||||
@@ -3610,6 +3524,17 @@
|
||||
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();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3937,9 +3862,7 @@
|
||||
`<br/><br/>Role: ${node.role_name}` +
|
||||
`<br/>Hardware: ${node.hardware_model_name}` +
|
||||
(node.firmware_version != null ? `<br/>Firmware: ${node.firmware_version}` : '') +
|
||||
(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"}` : '');
|
||||
`<br/>OK to MQTT: ${node.ok_to_mqtt}`;
|
||||
|
||||
if(node.battery_level){
|
||||
if(node.battery_level > 100){
|
||||
@@ -4060,9 +3983,15 @@
|
||||
|
||||
</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&code=45dcd3187c04c1eddf214bb44c8686a9" alt="web stats" width="1" height="1" />web stats</a></noscript>
|
||||
<!-- 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>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
205
src/stats.js
Normal file
205
src/stats.js
Normal file
@@ -0,0 +1,205 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user