Compare commits

..

103 Commits

Author SHA1 Message Date
Anton Roslund
1ee526caf7 Collect is_unmessagable, and public_key. 2025-08-06 19:48:26 +02:00
Anton Roslund
a7b99c3027 Merge pull request #21 from Roslund/use-protobuf-submodule
Load protobufs from submodule
2025-08-04 12:29:40 +02:00
Anton Roslund
b668892248 Merge pull request #18 from Roslund/dependabot/npm_and_yarn/prisma/client-6.13.0
Bump @prisma/client from 6.12.0 to 6.13.0
2025-08-04 07:39:14 +02:00
Anton Roslund
0053b9f774 Merge pull request #19 from Roslund/dependabot/npm_and_yarn/prisma-6.13.0
Bump prisma from 6.12.0 to 6.13.0
2025-08-04 07:39:01 +02:00
dependabot[bot]
435c122c21 Bump prisma from 6.12.0 to 6.13.0
Bumps [prisma](https://github.com/prisma/prisma/tree/HEAD/packages/cli) from 6.12.0 to 6.13.0.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/6.13.0/packages/cli)

---
updated-dependencies:
- dependency-name: prisma
  dependency-version: 6.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 05:36:35 +00:00
Anton Roslund
c5028f911f Merge pull request #20 from Roslund/dependabot/npm_and_yarn/mqtt-5.14.0
Bump mqtt from 5.13.2 to 5.14.0
2025-08-04 07:36:04 +02:00
Anton Roslund
7fee84de77 Merge pull request #17 from Roslund/dependabot/npm_and_yarn/jest-30.0.5
Bump jest from 30.0.4 to 30.0.5
2025-08-04 07:35:11 +02:00
Anton Roslund
162f8da79c Load protobufs from submodule 2025-08-03 21:05:33 +02:00
dependabot[bot]
ca9d1d9de0 Bump mqtt from 5.13.2 to 5.14.0
Bumps [mqtt](https://github.com/mqttjs/MQTT.js) from 5.13.2 to 5.14.0.
- [Release notes](https://github.com/mqttjs/MQTT.js/releases)
- [Changelog](https://github.com/mqttjs/MQTT.js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mqttjs/MQTT.js/compare/v5.13.2...v5.14.0)

---
updated-dependencies:
- dependency-name: mqtt
  dependency-version: 5.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 21:20:37 +00:00
dependabot[bot]
8eb5c695f0 Bump @prisma/client from 6.12.0 to 6.13.0
Bumps [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client) from 6.12.0 to 6.13.0.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/6.13.0/packages/client)

---
updated-dependencies:
- dependency-name: "@prisma/client"
  dependency-version: 6.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-29 20:45:41 +00:00
Anton Roslund
e11367544d Missed to remove firmware version from update. 2025-07-23 12:27:11 +02:00
Anton Roslund
cbcbeb9a22 Add jitter to imprecise positions 2025-07-23 08:07:33 +02:00
Anton Roslund
4c745123c1 Show ok to MQTT on map 2025-07-23 08:05:29 +02:00
Anton Roslund
800dcfef78 Only change firmware version and ok_to_mqtt if bitfield is present. 2025-07-23 08:04:04 +02:00
dependabot[bot]
e27f92d5c6 Bump jest from 30.0.4 to 30.0.5
Bumps [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) from 30.0.4 to 30.0.5.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-22 20:51:12 +00:00
Anton Roslund
772faa550b Merge pull request #14 from Roslund/dependabot/npm_and_yarn/compression-1.8.1
Bump compression from 1.8.0 to 1.8.1
2025-07-18 20:45:31 +02:00
Anton Roslund
a716d401a9 Merge pull request #15 from Roslund/dependabot/npm_and_yarn/npm_and_yarn-96c788614a
Bump the npm_and_yarn group with 2 updates
2025-07-18 20:45:10 +02:00
dependabot[bot]
aeeb95554b Bump the npm_and_yarn group with 2 updates
Bumps the npm_and_yarn group with 2 updates: [on-headers](https://github.com/jshttp/on-headers) and [compression](https://github.com/expressjs/compression).


Updates `on-headers` from 1.0.2 to 1.1.0
- [Release notes](https://github.com/jshttp/on-headers/releases)
- [Changelog](https://github.com/jshttp/on-headers/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0)

Updates `compression` from 1.8.0 to 1.8.1
- [Release notes](https://github.com/expressjs/compression/releases)
- [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/compression/compare/1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: on-headers
  dependency-version: 1.1.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: compression
  dependency-version: 1.8.1
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-18 13:11:12 +00:00
dependabot[bot]
bc78a0b0e3 Bump compression from 1.8.0 to 1.8.1
---
updated-dependencies:
- dependency-name: compression
  dependency-version: 1.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-17 20:47:11 +00:00
Anton Roslund
aa57eb543f Merge pull request #13 from Roslund/dependabot/npm_and_yarn/prisma/client-6.12.0
Bump @prisma/client from 6.11.1 to 6.12.0
2025-07-16 12:07:41 +02:00
Anton Roslund
d5993aebb8 Merge pull request #12 from Roslund/dependabot/npm_and_yarn/prisma-6.12.0
Bump prisma from 6.11.1 to 6.12.0
2025-07-16 12:07:21 +02:00
dependabot[bot]
2b2ca982a0 Bump @prisma/client from 6.11.1 to 6.12.0
Bumps [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client) from 6.11.1 to 6.12.0.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/6.12.0/packages/client)

---
updated-dependencies:
- dependency-name: "@prisma/client"
  dependency-version: 6.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 20:59:09 +00:00
dependabot[bot]
d685826f15 Bump prisma from 6.11.1 to 6.12.0
Bumps [prisma](https://github.com/prisma/prisma/tree/HEAD/packages/cli) from 6.11.1 to 6.12.0.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/6.12.0/packages/cli)

---
updated-dependencies:
- dependency-name: prisma
  dependency-version: 6.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 20:58:48 +00:00
Anton Roslund
91f98a6a09 Merge pull request #9 from Roslund/dependabot/npm_and_yarn/prisma-6.11.1
Bump prisma from 5.16.1 to 6.11.1
2025-07-15 11:17:29 +02:00
Anton Roslund
3452efb1b7 Merge pull request #3 from Roslund/dependabot/npm_and_yarn/prisma/client-6.11.1
Bump @prisma/client from 5.16.1 to 6.11.1
2025-07-15 11:17:17 +02:00
dependabot[bot]
54e83e5cdd Bump prisma from 5.16.1 to 6.11.1
Bumps [prisma](https://github.com/prisma/prisma/tree/HEAD/packages/cli) from 5.16.1 to 6.11.1.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/6.11.1/packages/cli)

---
updated-dependencies:
- dependency-name: prisma
  dependency-version: 6.11.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 09:16:18 +00:00
Anton Roslund
d3ac48f044 Merge pull request #5 from Roslund/dependabot/npm_and_yarn/jest-30.0.4
Bump jest from 29.7.0 to 30.0.4
2025-07-15 11:14:56 +02:00
Anton Roslund
24b90f6710 Merge pull request #4 from Roslund/dependabot/npm_and_yarn/protobufjs-7.5.3
Bump protobufjs from 7.3.2 to 7.5.3
2025-07-15 11:14:08 +02:00
dependabot[bot]
a77518facf Bump @prisma/client from 5.16.1 to 6.11.1
Bumps [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client) from 5.16.1 to 6.11.1.
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/6.11.1/packages/client)

---
updated-dependencies:
- dependency-name: "@prisma/client"
  dependency-version: 6.11.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 09:11:01 +00:00
Anton Roslund
c36a797967 Merge pull request #8 from Roslund/dependabot/npm_and_yarn/compression-1.8.0
Bump compression from 1.7.4 to 1.8.0
2025-07-15 11:09:35 +02:00
dependabot[bot]
5c83393cb1 Bump jest from 29.7.0 to 30.0.4
Bumps [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) from 29.7.0 to 30.0.4.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.0.4/packages/jest)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.0.4
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 08:31:31 +00:00
dependabot[bot]
5c0a7db8c6 Bump compression from 1.7.4 to 1.8.0
Bumps [compression](https://github.com/expressjs/compression) from 1.7.4 to 1.8.0.
- [Release notes](https://github.com/expressjs/compression/releases)
- [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/compression/compare/1.7.4...1.8.0)

---
updated-dependencies:
- dependency-name: compression
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 08:31:10 +00:00
Anton Roslund
201ba0d399 Merge pull request #7 from Roslund/dependabot/npm_and_yarn/command-line-args-6.0.1
Bump command-line-args from 5.2.1 to 6.0.1
2025-07-15 10:30:40 +02:00
dependabot[bot]
b31d10d434 Bump command-line-args from 5.2.1 to 6.0.1
Bumps [command-line-args](https://github.com/75lb/command-line-args) from 5.2.1 to 6.0.1.
- [Release notes](https://github.com/75lb/command-line-args/releases)
- [Commits](https://github.com/75lb/command-line-args/compare/v5.2.1...v6.0.1)

---
updated-dependencies:
- dependency-name: command-line-args
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 08:30:31 +00:00
dependabot[bot]
96cc1ad1d5 Bump protobufjs from 7.3.2 to 7.5.3
Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.3.2 to 7.5.3.
- [Release notes](https://github.com/protobufjs/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.3.2...protobufjs-v7.5.3)

---
updated-dependencies:
- dependency-name: protobufjs
  dependency-version: 7.5.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 08:29:57 +00:00
Anton Roslund
ce2121d964 Merge pull request #6 from Roslund/dependabot/npm_and_yarn/command-line-usage-7.0.3
Bump command-line-usage from 7.0.1 to 7.0.3
2025-07-15 10:29:18 +02:00
Anton Roslund
0c5b330681 Merge pull request #10 from Roslund/dependabot/npm_and_yarn/mqtt-5.13.2
Bump mqtt from 5.7.3 to 5.13.2
2025-07-15 10:27:58 +02:00
dependabot[bot]
2d1dd6fbb2 Bump mqtt from 5.7.3 to 5.13.2
Bumps [mqtt](https://github.com/mqttjs/MQTT.js) from 5.7.3 to 5.13.2.
- [Release notes](https://github.com/mqttjs/MQTT.js/releases)
- [Changelog](https://github.com/mqttjs/MQTT.js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mqttjs/MQTT.js/compare/v5.7.3...v5.13.2)

---
updated-dependencies:
- dependency-name: mqtt
  dependency-version: 5.13.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 14:22:18 +00:00
dependabot[bot]
9e17347a7a Bump command-line-usage from 7.0.1 to 7.0.3
Bumps [command-line-usage](https://github.com/75lb/command-line-usage) from 7.0.1 to 7.0.3.
- [Release notes](https://github.com/75lb/command-line-usage/releases)
- [Commits](https://github.com/75lb/command-line-usage/compare/v7.0.1...v7.0.3)

---
updated-dependencies:
- dependency-name: command-line-usage
  dependency-version: 7.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 13:10:57 +00:00
Anton Roslund
6619e42bad Create dependabot.yml 2025-07-14 12:54:35 +02:00
Anton Roslund
e95bac1063 Update express to v5.0.0 2025-07-08 16:54:59 +02:00
liamcottle
3eed255eb4 use purple lines between position history markers 2025-07-08 16:37:13 +02:00
liamcottle
2bc82ae7a3 add device image for STATION_G2 2025-07-08 16:34:36 +02:00
liamcottle
9d7a60efa5 remove white background from device images 2025-07-08 16:31:06 +02:00
liamcottle
8c064ac6e6 rotate and optimise image 2025-07-08 16:30:57 +02:00
Travis Hardiman
b3bb02fede replace U+00BA º (not degrees) with U+00B0 ° (degrees)
U+00BA º MASCULINE ORDINAL INDICATOR vs. U+00B0 ° DEGREE SIGN (&deg;)
2025-07-08 16:30:42 +02:00
Iris
11f5898996 Add files via upload 2025-07-08 16:30:25 +02:00
sgtwilko
17be3cb6a9 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-07-08 16:30:10 +02:00
Anton Roslund
d95af37be5 Add backbone layers 2025-06-27 11:15:52 +02:00
Anton Roslund
e30fb12aa8 More relaxed snr limits 2025-06-26 22:35:34 +02:00
Anton Roslund
6691df73f5 Add IAQ metrics 2025-06-26 22:33:37 +02:00
Anton Roslund
9b69d0ce27 Additional device images 2025-05-13 19:46:23 +02:00
Anton Roslund
cf3f053e12 Found better way to calculate neighbour offset. 2025-04-28 23:26:54 +02:00
Anton Roslund
305f091142 Revert "add configNeighboursPolylineOffset"
This reverts commit 4d1bdba6e0.
2025-04-28 23:22:16 +02:00
Anton Roslund
4d1bdba6e0 add configNeighboursPolylineOffset 2025-04-27 09:10:34 +02:00
Anton Roslund
6210f04ea5 Keep track of the names a node has been using 2025-04-26 22:09:31 +02:00
Anton Roslund
99e31d8692 Add Battery and ChannelUtilization tables 2025-04-26 15:58:56 +02:00
Anton Roslund
55cbdb63ba Add channel-utilization-stats 2025-04-20 22:19:07 +02:00
Anton Roslund
c251c5adb8 Filter packages not sent over LoRa from stats 2025-04-20 11:38:48 +02:00
Anton Roslund
189c89a7ca still testing 2025-04-19 09:34:40 +02:00
Anton Roslund
8b84a3a91c testing in production 2025-04-19 09:32:32 +02:00
Anton Roslund
6cd07fe314 adding battery-stats API 2025-04-19 09:28:49 +02:00
Anton Roslund
fac5a91829 include PKI packages in portnum-counts 2025-04-16 19:26:02 +02:00
Anton Roslund
2fbaab81c5 cleanup of unused apis and functions 2025-04-16 19:16:27 +02:00
Anton Roslund
bfb845ac37 Exclude map reports from from portnum-counts 2025-04-16 19:11:14 +02:00
Anton Roslund
274f0b8efa updates stats for testing 2025-04-16 18:52:51 +02:00
Anton Roslund
c8f322f012 Collect packet_id for ServiceEnvelopes 2025-04-15 20:49:27 +02:00
Anton Roslund
f35a8876f9 fix firmware version bug 2025-04-15 18:03:56 +02:00
Anton Roslund
af2a663dab Collect portnums for ServiceEnvelopes 2025-04-15 17:55:04 +02:00
Anton Roslund
342c8dc87a collect ok-to-mqtt falg 2025-04-15 17:27:15 +02:00
Anton Roslund
ff8bb07f7f Capture firmware version based on bitfield 2025-04-14 19:33:03 +02:00
Anton Roslund
9411c9e4cc remove LoRa region from sidebar and tooltip 2025-04-14 17:31:45 +02:00
Anton Roslund
7ab62a9968 Don't distinguish mqtt nodes with color 2025-04-14 17:21:33 +02:00
Anton Roslund
a3c4667f34 Use self reported height for terrain graph 2025-04-13 08:30:44 +02:00
Anton Roslund
700ee298c4 Get PortNums from protobufs 2025-04-09 10:03:46 +02:00
Anton Roslund
4c6bccc058 filter map reports from portnum-counts 2025-04-07 21:41:06 +02:00
Anton Roslund
cdcefee641 add portnum-counts stats api 2025-04-07 20:52:59 +02:00
Anton Roslund
6e9d425bda add most active nodes stats 2025-03-31 21:11:49 +02:00
Anton Roslund
984771925f don't sort it 2025-03-31 18:59:47 +02:00
Anton Roslund
cadc78d1d4 fix hardware stats API 2025-03-30 16:43:20 +02:00
Anton Roslund
fd36e1b0a2 Add position-precision api 2025-03-30 16:17:51 +02:00
Anton Roslund
c3c92b47f1 Move stats api to separate file 2025-03-30 16:17:26 +02:00
Anton Roslund
48e25dd352 add add/v1/messages-per-hour 2025-03-27 08:38:47 +01:00
Anton Roslund
708451b027 Add CROS headeders to /api requests 2025-03-27 08:32:04 +01:00
Anton Roslund
37e54c76a6 Add openssl to dockerfile 2025-03-27 08:31:16 +01:00
Anton Roslund
b319115fd2 Fix unnecessary line break 2025-03-18 22:33:33 +01:00
Anton Roslund
1eb0b7eeea Add yellow color to snr 2025-03-16 11:32:15 +01:00
Anton Roslund
67fc07d326 Fix terrain tooltip 2025-03-16 11:27:48 +01:00
Anton Roslund
24df50889d Cleanup terrain graphs 2025-03-16 11:02:41 +01:00
Anton Roslund
1f93c8eb9e Add analytics 2025-03-16 11:02:27 +01:00
Anton Roslund
566b8d6086 Add 30d Graphs 2025-03-09 09:40:42 +01:00
Anton Roslund
2b6156ff07 Default to showing all nodes on map, no clustering 2025-03-09 09:28:14 +01:00
Anton Roslund
3c12e5fbe9 Style header 2025-03-09 09:27:43 +01:00
Anton Roslund
ec8093a1ca whitespace 2025-03-05 00:14:39 +01:00
Anton Roslund
55c7a45060 Default to zoom over Stockholm 2025-03-05 00:10:56 +01:00
Anton Roslund
471d8dded6 Update info 2025-03-05 00:10:30 +01:00
Anton Roslund
045008a9df Remove useless buttons 2025-03-05 00:09:46 +01:00
Anton Roslund
fd50a4adeb Reskinn 2025-03-05 00:09:07 +01:00
Anton Roslund
efdf5f850d Merge branch 'liamcottle-master' 2025-01-27 19:02:16 +01:00
Anton Roslund
15bd3ebdc8 Merge 2025-01-27 19:00:02 +01:00
Roslund
a73d858ebd Remove Service Announcement 2024-11-11 19:27:36 +01:00
Roslund
af92253092 Remove neighbourlines mqtt warning 2024-11-11 19:26:03 +01:00
Roslund
7e5a026987 Remove analytics 2024-11-11 19:24:27 +01:00
28 changed files with 3381 additions and 1841 deletions

View File

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

13
.github/dependabot.yml vendored Normal file
View 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
View File

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

3
.gitmodules vendored Normal file
View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Submodule src/protobufs added at 1ecf94da98

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

View File

@@ -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&amp;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
View 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;